From 9db320ba65d91ff8c09169b359ded4bfff5196db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 17 Aug 2012 17:00:01 +0200 Subject: Use proper relative/absolute imports. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/factory/__init__.py b/factory/__init__.py index d2267f0..fd37c74 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -23,7 +23,7 @@ __version__ = '1.2.0' __author__ = 'Raphaël Barrois ' -from base import ( +from .base import ( Factory, StubFactory, DjangoModelFactory, @@ -51,7 +51,7 @@ from base import ( MOGO_BUILD, ) -from declarations import ( +from .declarations import ( LazyAttribute, Iterator, InfiniteIterator, -- cgit v1.2.3 From b093cc6a35c884b926e0c2bc5928b330cccd4e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 17 Aug 2012 17:02:48 +0200 Subject: [py3] Remove calls to iteritems(). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 2 +- factory/containers.py | 4 ++-- tests/test_base.py | 2 +- tests/test_using.py | 14 +++++++------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/factory/base.py b/factory/base.py index a3d91b0..240170c 100644 --- a/factory/base.py +++ b/factory/base.py @@ -396,7 +396,7 @@ class BaseFactory(object): factory's declarations or in the extra kwargs. """ stub_object = containers.StubObject() - for name, value in cls.attributes(create=False, extra=kwargs).iteritems(): + for name, value in cls.attributes(create=False, extra=kwargs).items(): setattr(stub_object, name, value) return stub_object diff --git a/factory/containers.py b/factory/containers.py index d50cb71..46a647f 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -136,7 +136,7 @@ class DeclarationDict(dict): Returns a dict containing all remaining elements. """ remaining = {} - for k, v in d.iteritems(): + for k, v in d.items(): if self.is_declaration(k, v): self[k] = v else: @@ -262,7 +262,7 @@ class AttributeBuilder(object): # Parse attribute declarations, wrapping SubFactory and # OrderedDeclaration. wrapped_attrs = {} - for k, v in self._attrs.iteritems(): + for k, v in self._attrs.items(): if isinstance(v, declarations.SubFactory): v = SubFactoryWrapper(v, self._subfields.get(k, {}), create) elif isinstance(v, declarations.OrderedDeclaration): diff --git a/tests/test_base.py b/tests/test_base.py index 7ec3d0e..c8109db 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -42,7 +42,7 @@ class FakeDjangoModel(object): return instance def __init__(self, **kwargs): - for name, value in kwargs.iteritems(): + for name, value in kwargs.items(): setattr(self, name, value) self.id = None diff --git a/tests/test_using.py b/tests/test_using.py index 7e141eb..7cebacb 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -56,7 +56,7 @@ class FakeModel(object): objects = FakeModelManager() def __init__(self, **kwargs): - for name, value in kwargs.iteritems(): + for name, value in kwargs.items(): setattr(self, name, value) self.id = None @@ -763,7 +763,7 @@ class SubFactoryTestCase(unittest.TestCase): def testSubFactoryAndSequence(self): class TestObject(object): def __init__(self, **kwargs): - for k, v in kwargs.iteritems(): + for k, v in kwargs.items(): setattr(self, k, v) class TestObjectFactory(factory.Factory): @@ -784,7 +784,7 @@ class SubFactoryTestCase(unittest.TestCase): def testSubFactoryOverriding(self): class TestObject(object): def __init__(self, **kwargs): - for k, v in kwargs.iteritems(): + for k, v in kwargs.items(): setattr(self, k, v) class TestObjectFactory(factory.Factory): @@ -793,7 +793,7 @@ class SubFactoryTestCase(unittest.TestCase): class OtherTestObject(object): def __init__(self, **kwargs): - for k, v in kwargs.iteritems(): + for k, v in kwargs.items(): setattr(self, k, v) class WrappingTestObjectFactory(factory.Factory): @@ -813,7 +813,7 @@ class SubFactoryTestCase(unittest.TestCase): class TestObject(object): def __init__(self, **kwargs): - for k, v in kwargs.iteritems(): + for k, v in kwargs.items(): setattr(self, k, v) class TestObjectFactory(factory.Factory): @@ -839,7 +839,7 @@ class SubFactoryTestCase(unittest.TestCase): class TestObject(object): def __init__(self, **kwargs): - for k, v in kwargs.iteritems(): + for k, v in kwargs.items(): setattr(self, k, v) class TestObjectFactory(factory.Factory): @@ -866,7 +866,7 @@ class SubFactoryTestCase(unittest.TestCase): """Test inheriting from a factory with subfactories, overriding.""" class TestObject(object): def __init__(self, **kwargs): - for k, v in kwargs.iteritems(): + for k, v in kwargs.items(): setattr(self, k, v) class TestObjectFactory(factory.Factory): -- cgit v1.2.3 From d4fcbd31f192420898923ed9d8e956acaed8396e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 17 Aug 2012 17:07:30 +0200 Subject: [py3] Disable 'scope bleeding' test on py3. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- tests/compat.py | 4 ++++ tests/test_using.py | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/compat.py b/tests/compat.py index 15fa3ae..813b2e4 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -21,6 +21,10 @@ """Compatibility tools for tests""" +import sys + +is_python2 = (sys.version_info[0] == 2) + try: import unittest2 as unittest except ImportError: diff --git a/tests/test_using.py b/tests/test_using.py index 7cebacb..3658d28 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -28,7 +28,7 @@ import warnings import factory -from .compat import unittest +from .compat import is_python2, unittest class TestObject(object): @@ -1038,7 +1038,8 @@ class IteratorTestCase(unittest.TestCase): for i, obj in enumerate(objs): self.assertEqual(i % 5, obj.one) - def test_infinite_iterator_list_comprehension(self): + @unittest.skipUnless(is_python2, "Scope bleeding fixed in Python3+") + def test_infinite_iterator_list_comprehension_scope_bleeding(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject -- cgit v1.2.3 From b152ba79ab355c231b6e5fd852bad546e06208d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 17 Aug 2012 17:08:33 +0200 Subject: [py3] Rename xrange to range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 6 +++--- tests/test_using.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/factory/base.py b/factory/base.py index 240170c..a5056fd 100644 --- a/factory/base.py +++ b/factory/base.py @@ -369,7 +369,7 @@ class BaseFactory(object): Returns: object list: the built instances """ - return [cls.build(**kwargs) for _ in xrange(size)] + return [cls.build(**kwargs) for _ in range(size)] @classmethod def create(cls, **kwargs): @@ -386,7 +386,7 @@ class BaseFactory(object): Returns: object list: the created instances """ - return [cls.create(**kwargs) for _ in xrange(size)] + return [cls.create(**kwargs) for _ in range(size)] @classmethod def stub(cls, **kwargs): @@ -410,7 +410,7 @@ class BaseFactory(object): Returns: object list: the stubbed instances """ - return [cls.stub(**kwargs) for _ in xrange(size)] + return [cls.stub(**kwargs) for _ in range(size)] @classmethod def generate(cls, strategy, **kwargs): diff --git a/tests/test_using.py b/tests/test_using.py index 3658d28..112604d 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1020,7 +1020,7 @@ class IteratorTestCase(unittest.TestCase): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject - one = factory.Iterator(xrange(10, 30)) + one = factory.Iterator(range(10, 30)) objs = TestObjectFactory.build_batch(20) @@ -1031,7 +1031,7 @@ class IteratorTestCase(unittest.TestCase): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject - one = factory.InfiniteIterator(xrange(5)) + one = factory.InfiniteIterator(range(5)) objs = TestObjectFactory.build_batch(20) @@ -1043,7 +1043,7 @@ class IteratorTestCase(unittest.TestCase): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject - one = factory.InfiniteIterator([j * 3 for j in xrange(5)]) + one = factory.InfiniteIterator([j * 3 for j in range(5)]) # Scope bleeding: j will end up in TestObjectFactory's scope. @@ -1053,7 +1053,7 @@ class IteratorTestCase(unittest.TestCase): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject - one = factory.InfiniteIterator([_j * 3 for _j in xrange(5)]) + one = factory.InfiniteIterator([_j * 3 for _j in range(5)]) # Scope bleeding : _j will end up in TestObjectFactory's scope. # But factory_boy ignores it, as a protected variable. @@ -1068,7 +1068,7 @@ class IteratorTestCase(unittest.TestCase): @factory.iterator def one(): - for i in xrange(10, 50): + for i in range(10, 50): yield i objs = TestObjectFactory.build_batch(20) @@ -1082,7 +1082,7 @@ class IteratorTestCase(unittest.TestCase): @factory.infinite_iterator def one(): - for i in xrange(5): + for i in range(5): yield i objs = TestObjectFactory.build_batch(20) -- cgit v1.2.3 From ac90ac4b3425cc79c164b3dc0bd13901bf814ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 17 Aug 2012 17:11:17 +0200 Subject: [py3] Various python3-compatibility fixes. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/containers.py | 2 +- factory/declarations.py | 2 +- tests/test_containers.py | 2 +- tests/test_using.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/factory/containers.py b/factory/containers.py index 46a647f..4ceb07f 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -62,7 +62,7 @@ class LazyStub(object): def __str__(self): return '' % ( - self.__target_class.__name__, self.__attrs.keys()) + self.__target_class.__name__, list(self.__attrs.keys())) def __fill__(self): """Fill this LazyStub, computing values of all defined attributes. diff --git a/factory/declarations.py b/factory/declarations.py index 77000f2..50a826f 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -129,7 +129,7 @@ class Iterator(OrderedDeclaration): self.iterator = iter(iterator) def evaluate(self, sequence, obj, containers=()): - return self.iterator.next() + return next(self.iterator) class InfiniteIterator(Iterator): diff --git a/tests/test_containers.py b/tests/test_containers.py index b1ed6ed..139e973 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -342,7 +342,7 @@ class AttributeBuilderTestCase(unittest.TestCase): ab = containers.AttributeBuilder(FakeFactory, {'one__blah': 1, 'two__bar': 2}) self.assertTrue(ab.has_subfields(sf)) - self.assertEqual(['one'], ab._subfields.keys()) + self.assertEqual(['one'], list(ab._subfields.keys())) self.assertEqual(2, ab._attrs['two__bar']) def test_sub_factory(self): diff --git a/tests/test_using.py b/tests/test_using.py index 112604d..ad62113 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1220,13 +1220,13 @@ class CircularTestCase(unittest.TestCase): def test_example(self): sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) - from cyclic import foo + from .cyclic import foo f = foo.FooFactory.build(bar__foo=None) self.assertEqual(42, f.x) self.assertEqual(13, f.bar.y) self.assertIsNone(f.bar.foo) - from cyclic import bar + from .cyclic import bar b = bar.BarFactory.build(foo__bar__foo__bar=None) self.assertEqual(13, b.y) self.assertEqual(42, b.foo.x) -- cgit v1.2.3 From c86d32b892c383fb18b0a5d7cebc7671e4e88ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 14 Nov 2012 23:15:55 +0100 Subject: Mix SelfAttribute with ContainerAttribute. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With a very simple syntax. Signed-off-by: Raphaël Barrois --- factory/declarations.py | 15 +++++++++- tests/test_declarations.py | 69 +++++++++++++++++++++++++++++++++------------- tests/test_using.py | 17 ++++++++++++ 3 files changed, 81 insertions(+), 20 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index 50a826f..fe1afa4 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -100,7 +100,11 @@ def deepgetattr(obj, name, default=_UNSPECIFIED): class SelfAttribute(OrderedDeclaration): """Specific OrderedDeclaration copying values from other fields. + If the field name starts with two dots or more, the lookup will be anchored + in the related 'parent'. + Attributes: + depth (int): the number of steps to go up in the containers chain attribute_name (str): the name of the attribute to copy. default (object): the default value to use if the attribute doesn't exist. @@ -108,11 +112,20 @@ 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('.')) + attribute_name = attribute_name[depth:] + + self.depth = depth self.attribute_name = attribute_name self.default = default def evaluate(self, sequence, obj, containers=()): - return deepgetattr(obj, self.attribute_name, self.default) + if self.depth > 1: + # Fetching from a parent + target = containers[self.depth - 2] + else: + target = obj + return deepgetattr(target, self.attribute_name, self.default) class Iterator(OrderedDeclaration): diff --git a/tests/test_declarations.py b/tests/test_declarations.py index c0b3539..4dea429 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -22,14 +22,13 @@ import datetime -from factory.declarations import deepgetattr, CircularSubFactory, OrderedDeclaration, \ - PostGenerationDeclaration, Sequence +from factory import declarations from .compat import unittest class OrderedDeclarationTestCase(unittest.TestCase): def test_errors(self): - decl = OrderedDeclaration() + decl = declarations.OrderedDeclaration() self.assertRaises(NotImplementedError, decl.evaluate, None, {}) @@ -44,29 +43,61 @@ class DigTestCase(unittest.TestCase): obj.a.b = self.MyObj(3) obj.a.b.c = self.MyObj(4) - self.assertEqual(2, deepgetattr(obj, 'a').n) - self.assertRaises(AttributeError, deepgetattr, obj, 'b') - self.assertEqual(2, deepgetattr(obj, 'a.n')) - self.assertEqual(3, deepgetattr(obj, 'a.c', 3)) - self.assertRaises(AttributeError, deepgetattr, obj, 'a.c.n') - self.assertRaises(AttributeError, deepgetattr, obj, 'a.d') - self.assertEqual(3, deepgetattr(obj, 'a.b').n) - self.assertEqual(3, deepgetattr(obj, 'a.b.n')) - self.assertEqual(4, deepgetattr(obj, 'a.b.c').n) - self.assertEqual(4, deepgetattr(obj, 'a.b.c.n')) - self.assertEqual(42, deepgetattr(obj, 'a.b.c.n.x', 42)) + self.assertEqual(2, declarations.deepgetattr(obj, 'a').n) + 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') + 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) + self.assertEqual(4, declarations.deepgetattr(obj, 'a.b.c.n')) + self.assertEqual(42, declarations.deepgetattr(obj, 'a.b.c.n.x', 42)) + + +class SelfAttributeTestCase(unittest.TestCase): + def test_standard(self): + a = declarations.SelfAttribute('foo.bar.baz') + self.assertEqual(0, a.depth) + self.assertEqual('foo.bar.baz', a.attribute_name) + self.assertEqual(declarations._UNSPECIFIED, a.default) + + def test_dot(self): + a = declarations.SelfAttribute('.bar.baz') + self.assertEqual(1, a.depth) + self.assertEqual('bar.baz', a.attribute_name) + self.assertEqual(declarations._UNSPECIFIED, a.default) + + def test_default(self): + a = declarations.SelfAttribute('bar.baz', 42) + self.assertEqual(0, a.depth) + self.assertEqual('bar.baz', a.attribute_name) + self.assertEqual(42, a.default) + + def test_parent(self): + a = declarations.SelfAttribute('..bar.baz') + self.assertEqual(2, a.depth) + self.assertEqual('bar.baz', a.attribute_name) + self.assertEqual(declarations._UNSPECIFIED, a.default) + + def test_grandparent(self): + a = declarations.SelfAttribute('...bar.baz') + self.assertEqual(3, a.depth) + self.assertEqual('bar.baz', a.attribute_name) + self.assertEqual(declarations._UNSPECIFIED, a.default) class PostGenerationDeclarationTestCase(unittest.TestCase): def test_extract_no_prefix(self): - decl = PostGenerationDeclaration() + decl = declarations.PostGenerationDeclaration() extracted, kwargs = decl.extract('foo', {'foo': 13, 'foo__bar': 42}) self.assertEqual(extracted, 13) self.assertEqual(kwargs, {'bar': 42}) def test_extract_with_prefix(self): - decl = PostGenerationDeclaration(extract_prefix='blah') + decl = declarations.PostGenerationDeclaration(extract_prefix='blah') extracted, kwargs = decl.extract('foo', {'foo': 13, 'foo__bar': 42, 'blah': 42, 'blah__baz': 1}) @@ -76,17 +107,17 @@ class PostGenerationDeclarationTestCase(unittest.TestCase): class CircularSubFactoryTestCase(unittest.TestCase): def test_lazyness(self): - f = CircularSubFactory('factory.declarations', 'Sequence', x=3) + f = declarations.CircularSubFactory('factory.declarations', 'Sequence', x=3) self.assertEqual(None, f.factory) self.assertEqual({'x': 3}, f.defaults) factory_class = f.get_factory() - self.assertEqual(Sequence, factory_class) + self.assertEqual(declarations.Sequence, factory_class) def test_cache(self): orig_date = datetime.date - f = CircularSubFactory('datetime', 'date') + f = declarations.CircularSubFactory('datetime', 'date') self.assertEqual(None, f.factory) factory_class = f.get_factory() diff --git a/tests/test_using.py b/tests/test_using.py index ad62113..38c9e9e 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -365,6 +365,23 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual(3, test_object.four) self.assertEqual(5, test_object.five) + def testSelfAttributeParent(self): + class TestModel2(FakeModel): + pass + + class TestModelFactory(FakeModelFactory): + FACTORY_FOR = TestModel + one = 3 + three = factory.SelfAttribute('..bar') + + class TestModel2Factory(FakeModelFactory): + FACTORY_FOR = TestModel2 + bar = 4 + two = factory.SubFactory(TestModelFactory, one=1) + + test_model = TestModel2Factory() + self.assertEqual(4, test_model.two.three) + def testSequenceDecorator(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject -- cgit v1.2.3 From 14640a61eca84a7624bc1994233529dcacc2417e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 14 Nov 2012 23:35:03 +0100 Subject: Remove deprecated _*_function class attributes. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setting them will still work as intended, though. Signed-off-by: Raphaël Barrois --- factory/base.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/factory/base.py b/factory/base.py index a5056fd..4b4f5af 100644 --- a/factory/base.py +++ b/factory/base.py @@ -498,10 +498,6 @@ class Factory(BaseFactory): class AssociatedClassError(RuntimeError): pass - # Customizing 'create' strategy, using a tuple to keep the creation function - # from turning it into an instance method. - _creation_function = (None,) - @classmethod def set_creation_function(cls, creation_function): """Set the creation function for this class. @@ -516,6 +512,8 @@ class Factory(BaseFactory): "Use of factory.set_creation_function is deprecated, and will be " "removed in the future. Please override Factory._create() instead.", PendingDeprecationWarning, 2) + # Customizing 'create' strategy, using a tuple to keep the creation function + # from turning it into an instance method. cls._creation_function = (creation_function,) @classmethod @@ -527,9 +525,9 @@ class Factory(BaseFactory): an instance will be created, and keyword arguments for the value of the fields of the instance. """ - creation_function = cls._creation_function[0] - if creation_function: - return creation_function + creation_function = getattr(cls, '_creation_function', ()) + if creation_function and creation_function[0]: + return creation_function[0] elif cls._create.__func__ == Factory._create.__func__: # Backwards compatibility. # Default creation_function and default _create() behavior. @@ -537,12 +535,6 @@ class Factory(BaseFactory): # on actual method implementation (otherwise, make_factory isn't # detected as 'default'). return DJANGO_CREATION - else: - return creation_function - - # Customizing 'build' strategy, using a tuple to keep the creation function - # from turning it into an instance method. - _building_function = (None,) @classmethod def set_building_function(cls, building_function): @@ -558,6 +550,8 @@ class Factory(BaseFactory): "Use of factory.set_building_function is deprecated, and will be " "removed in the future. Please override Factory._build() instead.", PendingDeprecationWarning, 2) + # Customizing 'build' strategy, using a tuple to keep the creation function + # from turning it into an instance method. cls._building_function = (building_function,) @classmethod @@ -569,7 +563,9 @@ class Factory(BaseFactory): an instance will be created, and keyword arguments for the value of the fields of the instance. """ - return cls._building_function[0] + building_function = getattr(cls, '_building_function', ()) + if building_function and building_function[0]: + return building_function[0] @classmethod def _prepare(cls, create, **kwargs): -- cgit v1.2.3 From a19f64cbfadc0e36b2ff9812980e23955276632c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 14 Nov 2012 23:42:30 +0100 Subject: Update docstrings. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/factory/base.py b/factory/base.py index 4b4f5af..20a3a6b 100644 --- a/factory/base.py +++ b/factory/base.py @@ -31,7 +31,8 @@ BUILD_STRATEGY = 'build' CREATE_STRATEGY = 'create' STUB_STRATEGY = 'stub' -# Creation functions. Use Factory.set_creation_function() to set a creation function appropriate for your ORM. +# Creation functions. Deprecated. +# Override Factory._create instead. def DJANGO_CREATION(class_to_create, **kwargs): warnings.warn( "Factories defaulting to Django's Foo.objects.create() is deprecated, " @@ -39,7 +40,8 @@ def DJANGO_CREATION(class_to_create, **kwargs): "factory.DjangoModelFactory instead.", PendingDeprecationWarning, 6) return class_to_create.objects.create(**kwargs) -# Building functions. Use Factory.set_building_function() to set a building functions appropriate for your ORM. +# Building functions. Deprecated. +# Override Factory._build instead. NAIVE_BUILD = lambda class_to_build, **kwargs: class_to_build(**kwargs) MOGO_BUILD = lambda class_to_build, **kwargs: class_to_build.new(**kwargs) -- cgit v1.2.3 From 5ec4a50edc67073e54218549d6985f934f94b88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 14 Nov 2012 23:42:46 +0100 Subject: Add an extension point for kwargs mangling. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 6 ++++++ tests/test_using.py | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/factory/base.py b/factory/base.py index 20a3a6b..59f37eb 100644 --- a/factory/base.py +++ b/factory/base.py @@ -569,6 +569,11 @@ class Factory(BaseFactory): if building_function and building_function[0]: return building_function[0] + @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. @@ -578,6 +583,7 @@ class Factory(BaseFactory): **kwargs: arguments to pass to the creation function """ target_class = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS) + kwargs = cls._adjust_kwargs(**kwargs) # Extract *args from **kwargs args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS) diff --git a/tests/test_using.py b/tests/test_using.py index 38c9e9e..f489f28 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -741,6 +741,28 @@ class NonKwargParametersTestCase(unittest.TestCase): self.assertEqual({'three': 3}, obj.kwargs) +class KwargAdjustTestCase(unittest.TestCase): + """Tests for the _adjust_kwargs method.""" + + def test_build(self): + class TestObject(object): + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + + @classmethod + def _adjust_kwargs(cls, **kwargs): + kwargs['foo'] = len(kwargs) + return kwargs + + obj = TestObjectFactory.build(x=1, y=2, z=3) + self.assertEqual({'x': 1, 'y': 2, 'z': 3, 'foo': 3}, obj.kwargs) + self.assertEqual((), obj.args) + + class SubFactoryTestCase(unittest.TestCase): def testSubFactory(self): class TestModel2(FakeModel): -- cgit v1.2.3 From 7fe9c7cc94f6ba69abdf45d09a2dcc8969503514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 14 Nov 2012 23:43:03 +0100 Subject: Add MogoFactory. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Factory subclass, along the lines of DjangoModelFactory. Signed-off-by: Raphaël Barrois --- factory/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/factory/base.py b/factory/base.py index 59f37eb..dde9f38 100644 --- a/factory/base.py +++ b/factory/base.py @@ -660,6 +660,14 @@ class DjangoModelFactory(Factory): return target_class._default_manager.create(*args, **kwargs) +class MogoFactory(Factory): + """Factory for mogo objects.""" + ABSTRACT_FACTORY = True + + def _build(cls, target_class, *args, **kwargs): + return target_class.new(*args, **kwargs) + + def make_factory(klass, **kwargs): """Create a new, simple factory for the given class.""" factory_name = '%sFactory' % klass.__name__ -- cgit v1.2.3 From 048fb1cc935a2ccbc5ca7af81c9390e218a1080b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 14 Nov 2012 23:51:09 +0100 Subject: Rename ABSTRACT_FACTORY to FACTORY_ABSTRACT. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit And add a deprecation warning too. Signed-off-by: Raphaël Barrois --- factory/base.py | 17 +++++++++++++---- tests/test_base.py | 2 +- tests/test_using.py | 4 ++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/factory/base.py b/factory/base.py index dde9f38..3d4e8c1 100644 --- a/factory/base.py +++ b/factory/base.py @@ -170,7 +170,7 @@ class FactoryMetaClass(BaseFactoryMetaClass): if FACTORY_CLASS_DECLARATION in attrs: return attrs[FACTORY_CLASS_DECLARATION] - # No specific associated calss was given, and one was defined for our + # No specific associated class was given, and one was defined for our # parent, use it. if inherited is not None: return inherited @@ -212,12 +212,21 @@ class FactoryMetaClass(BaseFactoryMetaClass): for construction of an associated class instance at a later time.""" parent_factories = get_factory_bases(bases) - if not parent_factories or attrs.get('ABSTRACT_FACTORY', False): + if not parent_factories or attrs.get('ABSTRACT_FACTORY', False) \ + or attrs.get('FACTORY_ABSTRACT', False): # If this isn't a subclass of Factory, or specifically declared # abstract, don't do anything special. if 'ABSTRACT_FACTORY' in attrs: + warnings.warn( + "The 'ABSTRACT_FACTORY' class attribute has been renamed " + "to 'FACTORY_ABSTRACT' for naming consistency, and will " + "be ignored in the future. Please upgrade class %s." % + class_name, DeprecationWarning, 2) attrs.pop('ABSTRACT_FACTORY') + if 'FACTORY_ABSTRACT' in attrs: + attrs.pop('FACTORY_ABSTRACT') + return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs) base = parent_factories[0] @@ -644,7 +653,7 @@ class DjangoModelFactory(Factory): handle those for non-numerical primary keys. """ - ABSTRACT_FACTORY = True + FACTORY_ABSTRACT = True @classmethod def _setup_next_sequence(cls): @@ -662,7 +671,7 @@ class DjangoModelFactory(Factory): class MogoFactory(Factory): """Factory for mogo objects.""" - ABSTRACT_FACTORY = True + FACTORY_ABSTRACT = True def _build(cls, target_class, *args, **kwargs): return target_class.new(*args, **kwargs) diff --git a/tests/test_base.py b/tests/test_base.py index c8109db..63fc62b 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -47,7 +47,7 @@ class FakeDjangoModel(object): self.id = None class FakeModelFactory(base.Factory): - ABSTRACT_FACTORY = True + FACTORY_ABSTRACT = True @classmethod def _create(cls, target_class, *args, **kwargs): diff --git a/tests/test_using.py b/tests/test_using.py index f489f28..e5ae374 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -62,7 +62,7 @@ class FakeModel(object): class FakeModelFactory(factory.Factory): - ABSTRACT_FACTORY = True + FACTORY_ABSTRACT = True @classmethod def _create(cls, target_class, *args, **kwargs): @@ -251,7 +251,7 @@ class UsingFactoryTestCase(unittest.TestCase): def test_abstract(self): class SomeAbstractFactory(factory.Factory): - ABSTRACT_FACTORY = True + FACTORY_ABSTRACT = True one = 'one' class InheritedFactory(SomeAbstractFactory): -- cgit v1.2.3 From a5184785b1229e02ec75506db675a2377b32c297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 14 Nov 2012 23:53:26 +0100 Subject: Keep FACTORY_FOR around. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit And add a test too. Signed-off-by: Raphaël Barrois --- factory/base.py | 3 --- tests/test_base.py | 8 ++++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/factory/base.py b/factory/base.py index 3d4e8c1..2b78e2b 100644 --- a/factory/base.py +++ b/factory/base.py @@ -236,9 +236,6 @@ class FactoryMetaClass(BaseFactoryMetaClass): associated_class = cls._discover_associated_class(class_name, attrs, inherited_associated_class) - # Remove the FACTORY_CLASS_DECLARATION attribute from attrs, if present. - attrs.pop(FACTORY_CLASS_DECLARATION, None) - # If inheriting the factory from a parent, keep a link to it. # This allows to use the sequence counters from the parents. if associated_class == inherited_associated_class: diff --git a/tests/test_base.py b/tests/test_base.py index 63fc62b..64570cd 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -64,6 +64,14 @@ class SafetyTestCase(unittest.TestCase): class FactoryTestCase(unittest.TestCase): + def test_factory_for(self): + class TestObjectFactory(base.Factory): + FACTORY_FOR = TestObject + + self.assertEqual(TestObject, TestObjectFactory.FACTORY_FOR) + obj = TestObjectFactory.build() + self.assertFalse(hasattr(obj, 'FACTORY_FOR')) + def testDisplay(self): class TestObjectFactory(base.Factory): FACTORY_FOR = FakeDjangoModel -- cgit v1.2.3 From 160ce5d291ba394c3ac8a42ed19c12cb00e08889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 15 Nov 2012 00:22:37 +0100 Subject: Update ChangeLog. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- docs/changelog.rst | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c64a89a..8e8034a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -27,8 +27,22 @@ ChangeLog *New:* - Add :class:`~factory.CircularSubFactory` to solve circular dependencies between factories - - 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. + - 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 + - Enhance :class:`~factory.SelfAttribute` to handle "container" attribute fetching + - Keep the :attr:`~factory.Factory.FACTORY_FOR` attribute in :class:`~factory.Factory` classes + - Provide a dedicated :class:`~factory.MogoFactory` subclass of :class:`~factory.Factory` + +*Pending deprecation:* + +The following features have been deprecated and will be removed in an upcoming release. + + - Usage of :meth:`~factory.Factory.set_creation_function` and :meth:`~factory.Factory.set_building_function` + are now deprecated + - The :attr:`~factory.Factory.ABSTRACT_FACTORY` attribute has been renamed to + :attr:`~factory.Factory.FACTORY_ABSTRACT`. + - Implicit associated class discovery is no longer supported, you must set the :attr:`~factory.Factory.FACTORY_FOR` + attribute on all :class:`~factory.Factory` subclasses 1.1.5 (09/07/2012) ------------------ @@ -99,7 +113,7 @@ ChangeLog - Allow custom build functions - Introduce :data:`~factory.MOGO_BUILD` build function - Add support for inheriting from multiple :class:`~factory.Factory` - - Base :class:`~factory.Factory` classes can now be declared :attr:`abstract `. - Provide :class:`~factory.DjangoModelFactory`, whose :class:`~factory.Sequence` counter starts at the next free database id - Introduce :class:`~factory.SelfAttribute`, a shortcut for ``factory.LazyAttribute(lambda o: o.foo.bar.baz``. -- cgit v1.2.3 From 7aa8e4612ef3ff02a62b68211254b66d4d040199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 15 Nov 2012 02:00:54 +0100 Subject: Update doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- README | 126 +++++++++++++++++++++++++++++++++++------------------ docs/index.rst | 73 +------------------------------ docs/internals.rst | 25 ----------- 3 files changed, 85 insertions(+), 139 deletions(-) mode change 100644 => 120000 docs/index.rst delete mode 100644 docs/internals.rst diff --git a/README b/README index b878d00..e25f788 100644 --- a/README +++ b/README @@ -4,32 +4,39 @@ factory_boy .. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master :target: http://travis-ci.org/rbarrois/factory_boy/ -factory_boy is a fixtures replacement based on thoughtbot's `factory_girl `_ . Like factory_girl it has a straightforward definition syntax, support for multiple build strategies (saved instances, unsaved instances, attribute dicts, and stubbed objects), and support for multiple factories for the same class, including factory inheritance. Django support is included, and support for other ORMs can be easily added. +factory_boy is a fixtures replacement based on thoughtbot's `factory_girl `_. -The official repository is at http://github.com/rbarrois/factory_boy; the documentation at http://readthedocs.org/docs/factoryboy/. +Its features include: -Credits -------- +- Straightforward syntax +- 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) -This README parallels the factory_girl README as much as possible; text and examples are reproduced for comparison purposes. Ruby users of factory_girl should feel right at home with factory_boy in Python. +The official repository is at http://github.com/rbarrois/factory_boy; the documentation at http://readthedocs.org/docs/factoryboy/. -factory_boy was originally written by Mark Sandstrom, and improved by Raphaël Barrois. +factory_boy supports Python 2.6 and 2.7 (Python 3 is in the works), and requires only the standard Python library. -Thank you Joe Ferris and thoughtbot for creating factory_girl. Download -------- Github: http://github.com/rbarrois/factory_boy/ -PyPI:: +PyPI: + +.. code-block:: sh - pip install factory_boy + $ pip install factory_boy -Source:: +Source: + +.. code-block:: sh + + $ git clone git://github.com/rbarrois/factory_boy/ + $ python setup.py install - # Download the source and run - python setup.py install Defining factories ------------------ @@ -56,6 +63,7 @@ Factories declare a set of attributes used to instantiate an object. The class o last_name = 'User' admin = True + Using factories --------------- @@ -72,8 +80,6 @@ factory_boy supports several different build strategies: build, create, attribut # Returns a dict of attributes that can be used to build a User instance attributes = UserFactory.attributes() - # Returns an object with all defined attributes stubbed out: - stub = UserFactory.stub() You can use the Factory class as a shortcut for the default build strategy: @@ -82,28 +88,16 @@ You can use the Factory class as a shortcut for the default build strategy: # Same as UserFactory.create() user = UserFactory() -The default strategy can be overridden: - -.. code-block:: python - - UserFactory.default_strategy = factory.BUILD_STRATEGY - user = UserFactory() - -The default strategy can also be overridden for all factories: - -.. code-block:: python - - # This will set the default strategy for all factories that don't define a default build strategy - factory.Factory.default_strategy = factory.BUILD_STRATEGY No matter which strategy is used, it's possible to override the defined attributes by passing keyword arguments: -.. code-block:: python +.. code-block:: pycon # Build a User instance and override first_name - user = UserFactory.build(first_name='Joe') - user.first_name - # => 'Joe' + >>> user = UserFactory.build(first_name='Joe') + >>> user.first_name + "Joe" + Lazy Attributes --------------- @@ -117,8 +111,11 @@ Most factory attributes can be added using static values that are evaluated when last_name = 'Blow' email = factory.LazyAttribute(lambda a: '{0}.{1}@example.com'.format(a.first_name, a.last_name).lower()) - UserFactory().email - # => 'joe.blow@example.com' +.. code-block:: pycon + + >>> UserFactory().email + "joe.blow@example.com" + The function passed to ``LazyAttribute`` is given the attributes defined for the factory up to the point of the LazyAttribute declaration. If a lambda won't cut it, the ``lazy_attribute`` decorator can be used to wrap a function: @@ -134,19 +131,22 @@ The function passed to ``LazyAttribute`` is given the attributes defined for the result = a.lhs + a.rhs # Or some other fancy calculation return result + Associations ------------ -Associated instances can also be generated using ``LazyAttribute``: +Associated instances can also be generated using ``SubFactory``: .. code-block:: python from models import Post class PostFactory(factory.Factory): - author = factory.LazyAttribute(lambda a: UserFactory()) + author = factory.SubFactory(UserFactory) + + +The associated object's strategy will be used: -The associated object's default strategy is always used: .. code-block:: python @@ -155,10 +155,11 @@ The associated object's default strategy is always used: post.id == None # => False post.author.id == None # => False - # Builds and saves a User, and then builds but does not save a Post + # Builds but does not save a User, and then builds but does not save a Post post = PostFactory.build() post.id == None # => True - post.author.id == None # => False + post.author.id == None # => True + Inheritance ----------- @@ -172,7 +173,8 @@ You can easily create multiple factories for the same class without repeating co class ApprovedPost(PostFactory): approved = True - approver = factory.LazyAttribute(lambda a: UserFactory()) + approver = factory.SubFactory(UserFactory) + Sequences --------- @@ -205,7 +207,7 @@ If you wish to use a custom method to set the initial ID for a sequence, you can @classmethod def _setup_next_sequence(cls): - return cls._associated_class.objects.values_list('id').order_by('-id')[0] + 1 + return cls.FACTORY_FOR.objects.values_list('id').order_by('-id')[0] + 1 Customizing creation -------------------- @@ -227,6 +229,8 @@ Factory._prepare method: user.save() return user +.. OHAI VIM** + Subfactories ------------ @@ -258,13 +262,13 @@ Abstract factories ------------------ If a ``Factory`` simply defines generic attribute declarations without being bound to a given class, -it should be marked 'abstract' by declaring ``ABSTRACT_FACTORY = True``. +it should be marked 'abstract' by declaring ``FACTORY_ABSTRACT = True``. Such factories cannot be built/created/.... .. code-block:: python class AbstractFactory(factory.Factory): - ABSTRACT_FACTORY = True + FACTORY_ABSTRACT = True foo = 'foo' >>> AbstractFactory() @@ -272,3 +276,41 @@ Such factories cannot be built/created/.... ... AttributeError: type object 'AbstractFactory' has no attribute '_associated_class' + +Contributing +============ + +factory_boy is distributed under the MIT License. + +Issues should be opened through `GitHub Issues `_; whenever possible, a pull request should be included. + +All pull request should pass the test suite, which can be launched simply with: + +.. code-block:: sh + + $ python setup.py test + + +In order to test coverage, please use: + +.. code-block:: sh + + $ pip install coverage + $ coverage erase; coverage run --branch setup.py test; coverage report + + +Contents, indices and tables +============================ + +.. toctree:: + :maxdepth: 2 + + examples + subfactory + post_generation + changelog + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index f91b830..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,72 +0,0 @@ -Welcome to Factory Boy's documentation! -======================================= - -factory_boy provides easy replacement for fixtures, based on thoughtbot's `factory_girl `_. - -It allows for an easy definition of factories, various build factories, factory inheritance, ... - - -Example -------- - -Defining a factory -"""""""""""""""""" - -Simply subclass the :py:class:`~factory.Factory` class, adding various class attributes which will be used as defaults:: - - import factory - - class MyUserFactory(factory.Factory): - FACTORY_FOR = MyUser # Define the related object - - # A simple attribute - first_name = 'Foo' - - # A 'sequential' attribute: each instance of the factory will have a different 'n' - last_name = factory.Sequence(lambda n: 'Bar' + n) - - # A 'lazy' attribute: computed from the values of other attributes - email = factory.LazyAttribute(lambda o: '%s.%s@example.org' % (o.first_name.lower(), o.last_name.lower())) - -Using a factory -""""""""""""""" - -Once defined, a factory can be instantiated through different methods:: - - # Calls MyUser(first_name='Foo', last_name='Bar0', email='foo.bar0@example.org') - >>> user = MyUserFactory.build() - - # Calls MyUser.objects.create(first_name='Foo', last_name='Bar1', email='foo.bar1@example.org') - >>> user = MyUserFactory.create() - - # Values can be overridden - >>> user = MyUserFactory.build(first_name='Baz') - >>> user.email - 'baz.bar2@example.org' - - # Additional values can be specified - >>> user = MyUserFactory.build(some_other_var=42) - >>> user.some_other_var - 42 - - - - -Contents: - -.. toctree:: - :maxdepth: 2 - - examples - subfactory - post_generation - internals - changelog - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/docs/index.rst b/docs/index.rst new file mode 120000 index 0000000..89a0106 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1 @@ +../README.rst \ No newline at end of file diff --git a/docs/internals.rst b/docs/internals.rst deleted file mode 100644 index 279c4e9..0000000 --- a/docs/internals.rst +++ /dev/null @@ -1,25 +0,0 @@ -Factory Boy's internals -====================== - - -declarations ------------- - -.. automodule:: factory.declarations - :members: - - -containers ----------- - -.. automodule:: factory.containers - :members: - - - -base ----- - -.. automodule:: factory.base - :members: - -- cgit v1.2.3 From 63c88fb17db0156606b87f4014b2b6275b261564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 15 Nov 2012 02:01:11 +0100 Subject: Update my email; MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/factory/__init__.py b/factory/__init__.py index fd37c74..a2a9f9a 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -21,7 +21,7 @@ # THE SOFTWARE. __version__ = '1.2.0' -__author__ = 'Raphaël Barrois ' +__author__ = 'Raphaël Barrois ' from .base import ( Factory, diff --git a/setup.py b/setup.py index 9bff8d1..57e701e 100755 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ setup( author='Mark Sandstrom', author_email='mark@deliciouslynerdy.com', maintainer='Raphaël Barrois', - maintainer_email='raphael.barrois@polytechnique.org', + maintainer_email='raphael.barrois+fboy@polytechnique.org', url='https://github.com/rbarrois/factory_boy', keywords=['factory_boy', 'factory', 'fixtures'], packages=['factory'], -- cgit v1.2.3 From b449fbf4d9f7d9b93b1c0f400cf562953b209534 Mon Sep 17 00:00:00 2001 From: Eduard Iskandarov Date: Mon, 19 Nov 2012 13:41:43 +0600 Subject: Fix pk lookup in _setup_next_sequence method. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #31 Signed-off-by: Raphaël Barrois --- factory/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/factory/base.py b/factory/base.py index 2b78e2b..8de652d 100644 --- a/factory/base.py +++ b/factory/base.py @@ -654,10 +654,10 @@ class DjangoModelFactory(Factory): @classmethod def _setup_next_sequence(cls): - """Compute the next available ID, based on the 'id' database field.""" + """Compute the next available PK, based on the 'pk' database field.""" try: - return 1 + cls._associated_class._default_manager.values_list('id', flat=True - ).order_by('-id')[0] + return 1 + cls._associated_class._default_manager.values_list('pk', flat=True + ).order_by('-pk')[0] except IndexError: return 1 -- cgit v1.2.3 From 85ded9c9dc0f1c0b57d360b4cf54fe1aba2f8ca7 Mon Sep 17 00:00:00 2001 From: obiwanus Date: Wed, 5 Dec 2012 20:52:51 +0400 Subject: Add classmethod decorator to child factories methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #33,#34 Signed-off-by: Raphaël Barrois --- factory/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/factory/base.py b/factory/base.py index 8de652d..3d3d383 100644 --- a/factory/base.py +++ b/factory/base.py @@ -661,6 +661,7 @@ class DjangoModelFactory(Factory): except IndexError: return 1 + @classmethod def _create(cls, target_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" return target_class._default_manager.create(*args, **kwargs) @@ -670,6 +671,7 @@ class MogoFactory(Factory): """Factory for mogo objects.""" FACTORY_ABSTRACT = True + @classmethod def _build(cls, target_class, *args, **kwargs): return target_class.new(*args, **kwargs) -- cgit v1.2.3 From c6182ccc61d2224275fb4af1f7807f91114e0bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 9 Dec 2012 01:59:00 +0100 Subject: Fix version numbering. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Somehow, I forgot that I had release 1.2.0 :/ Signed-off-by: Raphaël Barrois --- docs/changelog.rst | 9 ++++++++- factory/__init__.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8e8034a..c97f735 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,7 +21,7 @@ ChangeLog - Remove :meth:`~factory.Factory.set_building_function` / :meth:`~factory.Factory.set_creation_function` -1.2.0 (current) +1.3.0 (current) --------------- *New:* @@ -44,6 +44,13 @@ The following features have been deprecated and will be removed in an upcoming r - Implicit associated class discovery is no longer supported, you must set the :attr:`~factory.Factory.FACTORY_FOR` attribute on all :class:`~factory.Factory` subclasses +1.2.0 (09/08/2012) +------------------ + +*New:* + + - Add :class:`~factory.CircularSubFactory` to solve circular dependencies between factories + 1.1.5 (09/07/2012) ------------------ diff --git a/factory/__init__.py b/factory/__init__.py index a2a9f9a..a07ffa1 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__ = '1.2.0' +__version__ = '1.3.0-dev' __author__ = 'Raphaël Barrois ' from .base import ( -- cgit v1.2.3 From 8c3c45e441e589a4ede80bb8532803a382624332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 2 Jan 2013 10:48:43 +0100 Subject: Happy New Year! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- LICENSE | 3 ++- factory/__init__.py | 2 +- factory/base.py | 2 +- factory/containers.py | 2 +- factory/declarations.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/test_base.py | 2 +- tests/test_containers.py | 2 +- tests/test_declarations.py | 2 +- tests/test_using.py | 2 +- tests/test_utils.py | 2 +- 15 files changed, 16 insertions(+), 15 deletions(-) diff --git a/LICENSE b/LICENSE index 919f659..620dc61 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ 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 @@ -16,4 +17,4 @@ 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. \ No newline at end of file +THE SOFTWARE. diff --git a/factory/__init__.py b/factory/__init__.py index a07ffa1..0cf1b82 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011 Raphaël Barrois +# 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 diff --git a/factory/base.py b/factory/base.py index 3d3d383..c8c7f06 100644 --- a/factory/base.py +++ b/factory/base.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011 Raphaël Barrois +# 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 diff --git a/factory/containers.py b/factory/containers.py index 4ceb07f..31ee58b 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011 Raphaël Barrois +# 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 diff --git a/factory/declarations.py b/factory/declarations.py index fe1afa4..c64a0e5 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011 Raphaël Barrois +# 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 diff --git a/factory/utils.py b/factory/utils.py index e7cdf5f..90fdfc3 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011 Raphaël Barrois +# 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 diff --git a/tests/__init__.py b/tests/__init__.py index 80a96a4..7531edd 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011 Raphaël Barrois +# Copyright (c) 2011-2013 Raphaël Barrois from .test_base import * from .test_containers import * diff --git a/tests/compat.py b/tests/compat.py index 813b2e4..8d4f1d0 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011 Raphaël Barrois +# 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 diff --git a/tests/cyclic/bar.py b/tests/cyclic/bar.py index cc90930..a8c9670 100644 --- a/tests/cyclic/bar.py +++ b/tests/cyclic/bar.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2012 Raphaël Barrois +# 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 diff --git a/tests/cyclic/foo.py b/tests/cyclic/foo.py index e6f8896..7f00f12 100644 --- a/tests/cyclic/foo.py +++ b/tests/cyclic/foo.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2012 Raphaël Barrois +# 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 diff --git a/tests/test_base.py b/tests/test_base.py index 64570cd..ba88d3b 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 Raphaël Barrois +# 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 diff --git a/tests/test_containers.py b/tests/test_containers.py index 139e973..4f70e29 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 Raphaël Barrois +# 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 diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 4dea429..214adc0 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 Raphaël Barrois +# 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 diff --git a/tests/test_using.py b/tests/test_using.py index e5ae374..5453b95 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011 Raphaël Barrois +# 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 diff --git a/tests/test_utils.py b/tests/test_utils.py index 9aaafc1..b353c9d 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 Raphaël Barrois +# 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 -- cgit v1.2.3 From a10b58cd7897af617ce0926c2c08427f135db574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 10 Feb 2013 14:02:55 +0100 Subject: Improve README. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- README | 46 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/README b/README index e25f788..82bdafc 100644 --- a/README +++ b/README @@ -22,6 +22,7 @@ factory_boy supports Python 2.6 and 2.7 (Python 3 is in the works), and requires Download -------- +PyPI: http://pypi.python.org/pypi/factory_boy/ Github: http://github.com/rbarrois/factory_boy/ PyPI: @@ -46,10 +47,10 @@ Factories declare a set of attributes used to instantiate an object. The class o .. code-block:: python import factory - from models import User + from . import models class UserFactory(factory.Factory): - FACTORY_FOR = User + FACTORY_FOR = models.User first_name = 'John' last_name = 'Doe' @@ -57,7 +58,7 @@ 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 = User + FACTORY_FOR = models.User first_name = 'Admin' last_name = 'User' @@ -107,6 +108,7 @@ Most factory attributes can be added using static values that are evaluated when .. code-block:: python class UserFactory(factory.Factory): + FACTORY_FOR = 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()) @@ -139,9 +141,8 @@ Associated instances can also be generated using ``SubFactory``: .. code-block:: python - from models import Post - class PostFactory(factory.Factory): + FACTORY_FOR = models.Post author = factory.SubFactory(UserFactory) @@ -151,14 +152,18 @@ The associated object's strategy will be used: .. code-block:: python # Builds and saves a User and a Post - post = PostFactory() - post.id == None # => False - post.author.id == None # => False + >>> post = PostFactory() + >>> post.id is None # Post has been 'saved' + False + >>> post.author.id is None # post.author has been saved + False # Builds but does not save a User, and then builds but does not save a Post - post = PostFactory.build() - post.id == None # => True - post.author.id == None # => True + >>> post = PostFactory.build() + >>> post.id is None + True + >>> post.author.id is None + True Inheritance @@ -169,9 +174,11 @@ You can easily create multiple factories for the same class without repeating co .. code-block:: python class PostFactory(factory.Factory): + FACTORY_FOR = models.Post title = 'A title' class ApprovedPost(PostFactory): + # No need to replicate the FACTORY_FOR attribute approved = True approver = factory.SubFactory(UserFactory) @@ -184,31 +191,39 @@ 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 email = factory.Sequence(lambda n: 'person{0}@example.com'.format(n)) - UserFactory().email # => 'person0@example.com' - UserFactory().email # => 'person1@example.com' + >>> UserFactory().email + 'person0@example.com' + >>> UserFactory().email + 'person1@example.com' Sequences can be combined with lazy attributes: .. code-block:: python class UserFactory(factory.Factory): + FACTORY_FOR = models.User + name = 'Mark' email = factory.LazyAttributeSequence(lambda a, n: '{0}+{1}@example.com'.format(a.name, n).lower()) - UserFactory().email # => mark+0@example.com + >>> UserFactory().email + 'mark+0@example.com' If you wish to use a custom method to set the initial ID for a sequence, you can override the ``_setup_next_sequence`` class method: .. code-block:: python class MyFactory(factory.Factory): + FACTORY_FOR = MyClass @classmethod def _setup_next_sequence(cls): return cls.FACTORY_FOR.objects.values_list('id').order_by('-id')[0] + 1 + Customizing creation -------------------- @@ -231,6 +246,7 @@ Factory._prepare method: .. OHAI VIM** + Subfactories ------------ @@ -240,10 +256,12 @@ the global factory, using a simple syntax : ``field__attr=42`` will set the attr .. code-block:: python class InnerFactory(factory.Factory): + FACTORY_FOR = InnerClass foo = 'foo' bar = factory.LazyAttribute(lambda o: foo * 2) class ExternalFactory(factory.Factory): + FACTORY_FOR = OuterClass inner = factory.SubFactory(InnerFactory, foo='bar') >>> e = ExternalFactory() -- cgit v1.2.3 From 5fc48cc597e18e014e7bf6947bee8d371aababb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 10 Feb 2013 14:04:23 +0100 Subject: README: fix download section. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- README | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README b/README index 82bdafc..c41911d 100644 --- a/README +++ b/README @@ -23,15 +23,12 @@ Download -------- PyPI: http://pypi.python.org/pypi/factory_boy/ -Github: http://github.com/rbarrois/factory_boy/ - -PyPI: .. code-block:: sh $ pip install factory_boy -Source: +Source: http://github.com/rbarrois/factory_boy/ .. code-block:: sh -- cgit v1.2.3 From 853ecc70f0e1772b607554e98a714675c1dc5821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 27 Feb 2013 23:54:42 +0100 Subject: Add test for dual class/factory inheritance. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If it works properly, this would make pylint happy. Signed-off-by: Raphaël Barrois --- tests/test_using.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_using.py b/tests/test_using.py index 5453b95..fb0e8b0 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -249,6 +249,16 @@ class UsingFactoryTestCase(unittest.TestCase): test_object = TestObjectFactory.build() self.assertEqual(test_object.one, 'one') + def test_inheritance(self): + @factory.use_strategy(factory.BUILD_STRATEGY) + class TestObjectFactory(factory.Factory, TestObject): + FACTORY_FOR = TestObject + + one = 'one' + + test_object = TestObjectFactory() + self.assertEqual(test_object.one, 'one') + def test_abstract(self): class SomeAbstractFactory(factory.Factory): FACTORY_ABSTRACT = True -- cgit v1.2.3 From 5a1d9464543bcd7fdbeed1075d4461f213bede05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Feb 2013 01:33:51 +0100 Subject: Rewrite the whole documentation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- README | 186 ++------- docs/changelog.rst | 7 + docs/conf.py | 10 +- docs/examples.rst | 28 +- docs/ideas.rst | 11 + docs/internals.rst | 2 + docs/introduction.rst | 258 ++++++++++++ docs/orms.rst | 36 ++ docs/post_generation.rst | 91 ---- docs/recipes.rst | 62 +++ docs/reference.rst | 1027 ++++++++++++++++++++++++++++++++++++++++++++++ docs/subfactory.rst | 81 ---- 12 files changed, 1470 insertions(+), 329 deletions(-) create mode 100644 docs/ideas.rst create mode 100644 docs/internals.rst create mode 100644 docs/introduction.rst create mode 100644 docs/orms.rst delete mode 100644 docs/post_generation.rst create mode 100644 docs/recipes.rst create mode 100644 docs/reference.rst delete mode 100644 docs/subfactory.rst diff --git a/README b/README index c41911d..a819972 100644 --- a/README +++ b/README @@ -36,8 +36,16 @@ Source: http://github.com/rbarrois/factory_boy/ $ python setup.py install +Usage +----- + + +.. note:: This section provides a quick summary of factory_boy features. + A more detailed listing is available in the full documentation. + + 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: @@ -63,7 +71,7 @@ Factories declare a set of attributes used to instantiate an object. The class o Using factories ---------------- +""""""""""""""" factory_boy supports several different build strategies: build, create, attributes and stub: @@ -98,9 +106,13 @@ No matter which strategy is used, it's possible to override the defined attribut Lazy Attributes ---------------- +""""""""""""""" + +Most factory attributes can be added using static values that are evaluated when the factory is defined, +but some attributes (such as fields whose value is computed from other elements) +will need values assigned each time an instance is generated. -Most factory attributes can be added using static values that are evaluated when the factory is defined, but some attributes (such as associations and other attributes that must be dynamically generated) will need values assigned each time an instance is generated. These "lazy" attributes can be added as follows: +These "lazy" attributes can be added as follows: .. code-block:: python @@ -116,25 +128,28 @@ Most factory attributes can be added using static values that are evaluated when "joe.blow@example.com" -The function passed to ``LazyAttribute`` is given the attributes defined for the factory up to the point of the LazyAttribute declaration. If a lambda won't cut it, the ``lazy_attribute`` decorator can be used to wrap a function: +Sequences +""""""""" + +Unique values in a specific format (for example, e-mail addresses) can be generated using sequences. Sequences are defined by using ``Sequence`` or the decorator ``sequence``: .. code-block:: python - # Stub factories don't have an associated class. - class SumFactory(factory.StubFactory): - lhs = 1 - rhs = 1 + class UserFactory(factory.Factory): + FACTORY_FOR = models.User + email = factory.Sequence(lambda n: 'person{0}@example.com'.format(n)) - @lazy_attribute - def sum(a): - result = a.lhs + a.rhs # Or some other fancy calculation - return result + >>> UserFactory().email + 'person0@example.com' + >>> UserFactory().email + 'person1@example.com' Associations ------------- +"""""""""""" -Associated instances can also be generated using ``SubFactory``: +Some objects have a complex field, that should itself be defined from a dedicated factories. +This is handled by the ``SubFactory`` helper: .. code-block:: python @@ -163,137 +178,8 @@ The associated object's strategy will be used: True -Inheritance ------------ - -You can easily create multiple factories for the same class without repeating common attributes by using inheritance: - -.. code-block:: python - - class PostFactory(factory.Factory): - FACTORY_FOR = models.Post - title = 'A title' - - class ApprovedPost(PostFactory): - # No need to replicate the FACTORY_FOR attribute - approved = True - approver = factory.SubFactory(UserFactory) - - -Sequences ---------- - -Unique values in a specific format (for example, e-mail addresses) can be generated using sequences. Sequences are defined by using ``Sequence`` or the decorator ``sequence``: - -.. code-block:: python - - class UserFactory(factory.Factory): - FACTORY_FOR = models.User - email = factory.Sequence(lambda n: 'person{0}@example.com'.format(n)) - - >>> UserFactory().email - 'person0@example.com' - >>> UserFactory().email - 'person1@example.com' - -Sequences can be combined with lazy attributes: - -.. code-block:: python - - class UserFactory(factory.Factory): - FACTORY_FOR = models.User - - name = 'Mark' - email = factory.LazyAttributeSequence(lambda a, n: '{0}+{1}@example.com'.format(a.name, n).lower()) - - >>> UserFactory().email - 'mark+0@example.com' - -If you wish to use a custom method to set the initial ID for a sequence, you can override the ``_setup_next_sequence`` class method: - -.. code-block:: python - - class MyFactory(factory.Factory): - FACTORY_FOR = MyClass - - @classmethod - def _setup_next_sequence(cls): - return cls.FACTORY_FOR.objects.values_list('id').order_by('-id')[0] + 1 - - -Customizing creation --------------------- - -Sometimes, the default build/create by keyword arguments doesn't allow for enough -customization of the generated objects. In such cases, you should override the -Factory._prepare method: - -.. code-block:: python - - class UserFactory(factory.Factory): - @classmethod - def _prepare(cls, create, **kwargs): - password = kwargs.pop('password', None) - user = super(UserFactory, cls)._prepare(create, **kwargs) - if password: - user.set_password(password) - if create: - user.save() - return user - -.. OHAI VIM** - - -Subfactories ------------- - -If one of your factories has a field which is another factory, you can declare it as a ``SubFactory``. This allows to define attributes of that field when calling -the global factory, using a simple syntax : ``field__attr=42`` will set the attribute ``attr`` of the ``SubFactory`` defined in ``field`` to 42: - -.. code-block:: python - - class InnerFactory(factory.Factory): - FACTORY_FOR = InnerClass - foo = 'foo' - bar = factory.LazyAttribute(lambda o: foo * 2) - - class ExternalFactory(factory.Factory): - FACTORY_FOR = OuterClass - inner = factory.SubFactory(InnerFactory, foo='bar') - - >>> e = ExternalFactory() - >>> e.foo - 'bar' - >>> e.bar - 'barbar' - - >>> e2 : ExternalFactory(inner__bar='baz') - >>> e2.foo - 'bar' - >>> e2.bar - 'baz' - -Abstract factories ------------------- - -If a ``Factory`` simply defines generic attribute declarations without being bound to a given class, -it should be marked 'abstract' by declaring ``FACTORY_ABSTRACT = True``. -Such factories cannot be built/created/.... - -.. code-block:: python - - class AbstractFactory(factory.Factory): - FACTORY_ABSTRACT = True - foo = 'foo' - - >>> AbstractFactory() - Traceback (most recent call last): - ... - AttributeError: type object 'AbstractFactory' has no attribute '_associated_class' - - Contributing -============ +------------ factory_boy is distributed under the MIT License. @@ -315,15 +201,19 @@ In order to test coverage, please use: Contents, indices and tables -============================ +---------------------------- .. toctree:: :maxdepth: 2 + introduction + reference + orms + recipes examples - subfactory - post_generation + internals changelog + ideas * :ref:`genindex` * :ref:`modindex` diff --git a/docs/changelog.rst b/docs/changelog.rst index c97f735..2e7aaea 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -157,4 +157,11 @@ The following features have been deprecated and will be removed in an upcoming r - First version of factory_boy +Credits +------- + +* Initial version by Mark Sandstrom (2010) +* Developed by Raphaël Barrois since 2011 + + .. vim:et:ts=4:sw=4:tw=119:ft=rst: diff --git a/docs/conf.py b/docs/conf.py index fd9ded6..0ccaf29 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,7 +42,7 @@ master_doc = 'index' # General information about the project. project = u'Factory Boy' -copyright = u'2011, Raphaël Barrois, Mark Sandstrom' +copyright = u'2011-2013, 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 @@ -231,4 +231,10 @@ man_pages = [ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +intersphinx_mapping = { + 'http://docs.python.org/': None, + 'django': ( + 'http://docs.djangoproject.com/en/dev/', + 'http://docs.djangoproject.com/en/dev/_objects/', + ), +} diff --git a/docs/examples.rst b/docs/examples.rst index cac6bc6..aab990a 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -7,7 +7,10 @@ Here are some real-world examples of using FactoryBoy. Objects ------- -First, let's define a couple of objects:: +First, let's define a couple of objects: + + +.. code-block:: python class Account(object): def __init__(self, username, email): @@ -41,7 +44,10 @@ First, let's define a couple of objects:: Factories --------- -And now, we'll define the related factories:: +And now, we'll define the related factories: + + +.. code-block:: python import factory import random @@ -60,15 +66,18 @@ And now, we'll define the related factories:: FACTORY_FOR = objects.Profile account = factory.SubFactory(AccountFactory) - gender = random.choice([objects.Profile.GENDER_MALE, objects.Profile.GENDER_FEMALE]) + gender = factory.Iterator([objects.Profile.GENDER_MALE, objects.Profile.GENDER_FEMALE]) firstname = u'John' lastname = u'Doe' -We have now defined basic factories for our :py:class:`~Account` and :py:class:`~Profile` classes. +We have now defined basic factories for our :class:`~Account` and :class:`~Profile` classes. + +If we commonly use a specific variant of our objects, we can refine a factory accordingly: -If we commonly use a specific variant of our objects, we can refine a factory accordingly:: + +.. code-block:: python class FemaleProfileFactory(ProfileFactory): gender = objects.Profile.GENDER_FEMALE @@ -80,7 +89,10 @@ If we commonly use a specific variant of our objects, we can refine a factory ac Using the factories ------------------- -We can now use our factories, for tests:: +We can now use our factories, for tests: + + +.. code-block:: python import unittest @@ -112,7 +124,9 @@ We can now use our factories, for tests:: self.assertLess(stats.genders[objects.Profile.GENDER_FEMALE], 2) -Or for fixtures:: +Or for fixtures: + +.. code-block:: python from . import factories diff --git a/docs/ideas.rst b/docs/ideas.rst new file mode 100644 index 0000000..004f722 --- /dev/null +++ b/docs/ideas.rst @@ -0,0 +1,11 @@ +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 +* **factory-local fields**: Allow some fields to be available while building attributes, + but not passed to the associated class. + For instance, a ``global_kind`` field would be used to select values for many other fields. + diff --git a/docs/internals.rst b/docs/internals.rst new file mode 100644 index 0000000..a7402ff --- /dev/null +++ b/docs/internals.rst @@ -0,0 +1,2 @@ +Internals +========= diff --git a/docs/introduction.rst b/docs/introduction.rst new file mode 100644 index 0000000..d211a83 --- /dev/null +++ b/docs/introduction.rst @@ -0,0 +1,258 @@ +Introduction +============ + + +The purpose of factory_boy is to provide a default way of getting a new instance, +while still being able to override some fields on a per-call basis. + + +.. note:: This section will drive you through an overview of factory_boy's feature. + New users are advised to spend a few minutes browsing through this list + of useful helpers. + + Users looking for quick helpers may take a look at :doc:`recipes`, + while those needing detailed documentation will be interested in the :doc:`reference` section. + + +Basic usage +----------- + + +Factories declare a set of attributes used to instantiate an object, whose class is defined in the FACTORY_FOR attribute: + +- Subclass ``factory.Factory`` (or a more suitable subclass) +- Set its ``FACTORY_FOR`` attribute to the target class +- Add defaults for keyword args to pass to the associated class' ``__init__`` method + + +.. code-block:: python + + import factory + from . import base + + class UserFactory(factory.Factory): + FACTORY_FOR = base.User + + firstname = "John" + lastname = "Doe" + +You may now get ``base.User`` instances trivially: + +.. code-block:: pycon + + >>> john = UserFactory() + + +It is also possible to override the defined attributes by passing keyword arguments to the factory: + +.. code-block:: pycon + + >>> jack = UserFactory(firstname="Jack") + + + +A given class may be associated to many :class:`~factory.Factory` subclasses: + +.. code-block:: python + + class EnglishUserFactory(factory.Factory): + FACTORY_FOR = base.User + + firstname = "John" + lastname = "Doe" + lang = 'en' + + + class FrenchUserFactory(factory.Factory): + FACTORY_FOR = base.User + + firstname = "Jean" + lastname = "Dupont" + lang = 'fr' + + +.. code-block:: pycon + + >>> EnglishUserFactory() + + >>> FrenchUserFactory() + + + +Sequences +--------- + +When a field has a unique key, each object generated by the factory should have a different value for that field. +This is achieved with the :class:`~factory.Sequence` declaration: + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = models.User + + username = factory.Sequence(lambda n: 'user%d' % n) + +.. code-block:: pycon + + >>> UserFactory() + + >>> UserFactory() + + +.. note:: For more complex situations, you may also use the :meth:`~factory.@sequence` decorator: + + .. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = models.User + + @factory.sequence + def username(self, n): + return 'user%d' % n + + +LazyAttribute +------------- + +Some fields may be deduced from others, for instance the email based on the username. +The :class:`~factory.LazyAttribute` handles such cases: it should receive a function +taking the object being built and returning the value for the field: + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = models.User + + username = factory.Sequence(lambda n: 'user%d' % n) + email = factory.LazyAttribute(lambda obj: '%s@example.com' % obj.username) + +.. code-block:: pycon + + >>> UserFactory() + + + >>> # The LazyAttribute handles overridden fields + >>> UserFactory(username='john') + + + >>> # 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. + + +Inheritance +----------- + + +Once a "base" factory has been defined for a given class, +alternate versions can be easily defined through subclassing. + +The subclassed :class:`~factory.Factory` will inherit all declarations from its parent, +and update them with its own declarations: + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = base.User + + firstname = "John" + lastname = "Doe" + group = 'users' + + class AdminFactory(UserFactory): + admin = True + group = 'admins' + +.. code-block:: pycon + + >>> user = UserFactory() + >>> user + + >>> user.group + 'users' + + >>> admin = AdminFactory() + >>> admin + + >>> admin.group # The AdminFactory field has overridden the base field + 'admins' + + +Any argument of all factories in the chain can easily be overridden: + +.. code-block:: pycon + + >>> super_admin = AdminFactory(group='superadmins', lastname="Lennon") + >>> super_admin + + >>> super_admin.group # Overridden at call time + 'superadmins' + + +Non-kwarg arguments +------------------- + +Some classes take a few, non-kwarg arguments first. + +This is handled by the :data:`~factory.Factory.FACTORY_ARG_PARAMETERS` attribute: + +.. code-block:: python + + class MyFactory(factory.Factory): + FACTORY_FOR = MyClass + FACTORY_ARG_PARAMETERS = ('x', 'y') + + x = 1 + y = 2 + z = 3 + +.. code-block:: pycon + + >>> MyFactory(y=4) + + + +Strategies +---------- + +All factories support two built-in strategies: + +* ``build`` provides a local object +* ``create`` instantiates a local object, and saves it to the database. + +.. note:: For 1.X versions, the ``create`` will actually call ``AssociatedClass.objects.create``, + as for a Django model. + + Starting from 2.0, :meth:`factory.Factory.create` simply calls ``AssociatedClass(**kwargs)``. + You should use :class:`~factory.DjangoModelFactory` for Django models. + + +When a :class:`~factory.Factory` includes related fields (:class:`~factory.SubFactory`, :class:`~factory.RelatedFactory`), +the parent's strategy will be pushed onto related factories. + + +Calling a :class:`~factory.Factory` subclass will provide an object through the default strategy: + +.. code-block:: python + + class MyFactory(factory.Factory): + FACTORY_FOR = MyClass + +.. code-block:: pycon + + >>> MyFactory.create() + + + >>> MyFactory.build() + + + >>> MyFactory() # equivalent to MyFactory.create() + + + +The default strategy can ba changed by setting the class-level :attr:`~factory.Factory.default_strategy` attribute. + + diff --git a/docs/orms.rst b/docs/orms.rst new file mode 100644 index 0000000..eae31d9 --- /dev/null +++ b/docs/orms.rst @@ -0,0 +1,36 @@ +Using factory_boy with ORMs +=========================== + +.. currentmodule:: factory + + +factory_boy provides custom :class:`Factory` subclasses for various ORMs, +adding dedicated features. + + +Django +------ + + +The first versions of factory_boy were designed specifically for Django, +but the library has now evolved to be framework-independant. + +Most features should thus feel quite familiar to Django users. + +The :class:`DjangoModelFactory` subclass +""""""""""""""""""""""""""""""""""""""""" + +All factories for a Django :class:`~django.db.models.Model` should use the +:class:`DjangoModelFactory` base class. + + +.. class:: DjangoModelFactory(Factory) + + Dedicated class for Django :class:`~django.db.models.Model` factories. + + This class provides the following features: + + * :func:`~Factory.create()` uses :meth:`Model.objects.create() ` + * :func:`~Factory._setup_next_sequence()` selects the next unused primary key value + * When using :class:`~factory.RelatedFactory` attributes, the base object will be + :meth:`saved ` once all post-generation hooks have run. diff --git a/docs/post_generation.rst b/docs/post_generation.rst deleted file mode 100644 index d712abc..0000000 --- a/docs/post_generation.rst +++ /dev/null @@ -1,91 +0,0 @@ -PostGenerationHook -================== - - -Some objects expect additional method calls or complex processing for proper definition. -For instance, a ``User`` may need to have a related ``Profile``, where the ``Profile`` is built from the ``User`` object. - -To support this pattern, factory_boy provides the following tools: - - :py:class:`factory.PostGeneration`: this class allows calling a given function with the generated object as argument - - :py:func:`factory.post_generation`: decorator performing the same functions as :py:class:`~factory.PostGeneration` - - :py:class:`factory.RelatedFactory`: this builds or creates a given factory *after* building/creating the first Factory. - - -Passing arguments to a post-generation hook -------------------------------------------- - -A post-generation hook will be defined with a given attribute name. -When calling the ``Factory``, some arguments will be passed to the post-generation hook instead of being available for ``Factory`` building: - - - An argument with the same name as the post-generation hook attribute will be passed to the hook - - All arguments beginning with that name and ``__`` will be passed to the hook, after removing the prefix. - -Example:: - - class MyFactory(factory.Factory): - blah = factory.PostGeneration(lambda obj, create, extracted, **kwargs: 42) - - MyFactory( - blah=42, # Passed in the 'extracted' argument of the lambda - blah__foo=1, # Passed in kwargs as 'foo': 1 - blah__baz=2, # Passed in kwargs as 'baz': 2 - blah_bar=3, # Not passed - ) - -The prefix used for extraction can be changed by setting the ``extract_prefix`` argument of the hook:: - - class MyFactory(factory.Factory): - @factory.post_generation(extract_prefix='bar') - def foo(self, create, extracted, **kwargs): - self.foo = extracted - - MyFactory( - bar=42, # Will be passed to 'extracted' - bar__baz=13, # Will be passed as 'baz': 13 in kwargs - foo=2, # Won't be passed to the post-generation hook - ) - - -PostGeneration and @post_generation ------------------------------------ - -Both declarations wrap a function, which will be called with the following arguments: - - ``obj``: the generated factory - - ``create``: whether the factory was "built" or "created" - - ``extracted``: if the :py:class:`~factory.PostGeneration` was declared as attribute ``foo``, - and another value was given for ``foo`` when calling the ``Factory``, - that value will be available in the ``extracted`` parameter. - - other keyword args are extracted from those passed to the ``Factory`` with the same prefix as the name of the :py:class:`~factory.PostGeneration` attribute - - -RelatedFactory --------------- - -This is declared with the following arguments: - - ``factory``: the :py:class:`~factory.Factory` to call - - ``name``: the keyword to use when passing this object to the related :py:class:`~factory.Factory`; if empty, the object won't be passed to the related :py:class:`~factory.Factory` - - Extra keyword args which will be passed to the factory - -When the object is built, the keyword arguments passed to the related :py:class:`~factory.Factory` are: - - ``name: obj`` if ``name`` was passed when defining the :py:class:`~factory.RelatedFactory` - - extra keyword args defined in the :py:class:`~factory.RelatedFactory` definition, overridden by any prefixed arguments passed to the object definition - - -Example:: - - class RelatedObjectFactory(factory.Factory): - FACTORY_FOR = RelatedObject - one = 1 - two = 2 - related = None - - class ObjectWithRelatedFactory(factory.Factory): - FACTORY_FOR = SomeObject - foo = factory.RelatedFactory(RelatedObjectFactory, 'related', one=2) - - ObjectWithRelatedFactory(foo__two=3) - -The ``RelatedObject`` will be called with: - - ``one=2`` - - ``two=3`` - - ``related=`` diff --git a/docs/recipes.rst b/docs/recipes.rst new file mode 100644 index 0000000..8af0c8f --- /dev/null +++ b/docs/recipes.rst @@ -0,0 +1,62 @@ +Common recipes +============== + + +.. note:: Most recipes below take on Django model examples, but can also be used on their own. + + +Dependent objects (ForeignKey) +------------------------------ + +When one attribute is actually a complex field +(e.g a :class:`~django.db.models.ForeignKey` to another :class:`~django.db.models.Model`), +use the :class:`~factory.SubFactory` declaration: + + +.. code-block:: python + + # models.py + class User(models.Model): + first_name = models.CharField() + group = models.ForeignKey(Group) + + + # factories.py + import factory + from . import models + + class UserFactory(factory.Factory): + FACTORY_FOR = models.User + + first_name = factory.Sequence(lambda n: "Agent %03d" % n) + group = factory.SubFactory(GroupFactory) + + +Reverse dependencies (reverse ForeignKey) +----------------------------------------- + +When a related object should be created upon object creation +(e.g a reverse :class:`~django.db.models.ForeignKey` from another :class:`~django.db.models.Model`), +use a :class:`~factory.RelatedFactory` declaration: + + +.. code-block:: python + + # models.py + class User(models.Model): + pass + + class UserLog(models.Model): + user = models.ForeignKey(User) + action = models.CharField() + + + # factories.py + class UserFactory(factory.Factory): + FACTORY_FOR = models.User + + log = factory.RelatedFactory(UserLogFactory, 'user', action=models.UserLog.ACTION_CREATE) + + +When a :class:`UserFactory` is instantiated, factory_boy will call +``UserLogFactory(user=that_user, action=...)`` just before returning the created ``User``. diff --git a/docs/reference.rst b/docs/reference.rst new file mode 100644 index 0000000..289a9a8 --- /dev/null +++ b/docs/reference.rst @@ -0,0 +1,1027 @@ +Reference +========= + +.. currentmodule:: factory + +This section offers an in-depth description of factory_boy features. + +For internals and customization points, please refer to the :doc:`internals` section. + + +The :class:`Factory` class +-------------------------- + +.. class:: Factory + + The :class:`Factory` class is the base of factory_boy features. + + It accepts a few specific attributes (must be specified on class declaration): + + .. attribute:: FACTORY_FOR + + This required attribute describes the class of objects to generate. + It may only be absent if the factory has been marked abstract through + :attr:`ABSTRACT_FACTORY`. + + .. attribute:: ABSTRACT_FACTORY + + This attribute indicates that the :class:`Factory` subclass should not + be used to generate objects, but instead provides some extra defaults. + + .. attribute:: FACTORY_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` + attribute: + + .. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + FACTORY_ARG_PARAMETERS = ('login', 'email') + + login = 'john' + email = factory.LazyAttribute(lambda o: '%s@example.com' % o.login) + firstname = "John" + + .. code-block:: pycon + + >>> UserFactory() + + >>> User('john', 'john@example.com', firstname="John") # actual call + + **Base functions:** + + 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') + + 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 + adequate method: + + .. classmethod:: build(cls, **kwargs) + + Provides a new object, using the 'build' strategy. + + .. classmethod:: build_batch(cls, size, **kwargs) + + Provides a list of :obj:`size` instances from the :class:`Factory`, + through the 'build' strategy. + + + .. classmethod:: create(cls, **kwargs) + + Provides a new object, using the 'create' strategy. + + .. classmethod:: create_batch(cls, size, **kwargs) + + Provides a list of :obj:`size` instances from the :class:`Factory`, + through the 'create' strategy. + + + .. classmethod:: stub(cls, **kwargs) + + Provides a new stub + + .. classmethod:: stub_batch(cls, size, **kwargs) + + Provides a list of :obj:`size` stubs from the :class:`Factory`. + + + .. classmethod:: generate(cls, strategy, **kwargs) + + Provide a new instance, with the provided :obj:`strategy`. + + .. classmethod:: generate_batch(cls, strategy, size, **kwargs) + + Provides a list of :obj:`size` instances using the specified strategy. + + + .. classmethod:: simple_generate(cls, create, **kwargs) + + Provide a new instance, either built (``create=False``) or created (``create=True``). + + .. classmethod:: simple_generate_batch(cls, create, size, **kwargs) + + Provides a list of :obj:`size` instances, either built or created + according to :obj:`create`. + + + **Extension points:** + + A :class:`Factory` subclass may override a couple of class methods to adapt + its behaviour: + + .. classmethod:: _adjust_kwargs(cls, **kwargs) + + .. OHAI_VIM** + + 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 + phase. + + .. code-block:: python + + class UserFactory(factory.Factory): + + @classmethod + def _adjust_kwargs(cls, **kwargs): + # Ensure ``lastname`` is upper-case. + kwargs['lastname'] = kwargs['lastname'].upper() + return kwargs + + .. OHAI_VIM** + + + .. classmethod:: _setup_next_sequence(cls) + + This method will compute the first value to use for the sequence counter + of this factory. + + It is called when the first instance of the factory (or one of its subclasses) + is created. + + Subclasses may fetch the next free ID from the database, for instance. + + + .. classmethod:: _build(cls, target_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:`FACTORY_FOR`), 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) + + .. OHAI_VIM* + + The :meth:`_create` method is called whenever an instance needs to be + created. + It receives the same arguments as :meth:`_build`. + + Subclasses may override this for specific persistence backends: + + .. code-block:: python + + class BaseBackendFactory(factory.Factory): + ABSTRACT_FACTORY = True + + def _create(cls, target_class, *args, **kwargs): + obj = target_class(*args, **kwargs) + obj.save() + return obj + + .. OHAI_VIM* + + +.. _strategies: + +Strategies +"""""""""" + +factory_boy supports two main strategies for generating instances, plus stubs. + + +.. data:: BUILD_STRATEGY + + The 'build' strategy is used when an instance should be created, + 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. + + +.. data:: CREATE_STRATEGY + + The 'create' strategy builds and saves an instance into its appropriate datastore. + + This is the default strategy of factory_boy; it would typically instantiate an + object, then save it: + + .. code-block:: pycon + + >>> obj = self._associated_class(*args, **kwargs) + >>> obj.save() + >>> return obj + + .. OHAI_VIM* + + +.. function:: use_strategy(strategy) + + *Decorator* + + Change the default strategy of the decorated :class:`Factory` to the chosen :obj:`strategy`: + + .. code-block:: python + + @use_strategy(factory.BUILD_STRATEGY) + class UserBuildingFactory(UserFactory): + pass + + +.. 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 + require one to be present. + + Instead, it returns an instance of :class:`StubObject` whose attributes have been + set according to the declarations. + + +.. class:: StubObject(object) + + A plain, stupid object. No method, no helpers, simply a bunch of attributes. + + It is typically instantiated, then has its attributes set: + + .. code-block:: pycon + + >>> obj = StubObject() + >>> obj.x = 1 + >>> obj.y = 2 + + +.. class:: StubFactory(Factory) + + An :attr:`abstract ` :class:`Factory`, + with a default strategy set to :data:`STUB_STRATEGY`. + + +.. _declarations: + +Declarations +------------ + +LazyAttribute +""""""""""""" + +.. class:: LazyAttribute(method_to_call) + +The :class:`LazyAttribute` is a simple yet extremely powerful building brick +for extending a :class:`Factory`. + +It takes as argument a method to call (usually a lambda); that method should +accept the object being built as sole argument, and return a value. + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + username = 'john' + email = factory.LazyAttribute(lambda o: '%s@example.com' % o.username) + +.. code-block:: pycon + + >>> u = UserFactory() + >>> u.email + 'john@example.com' + + >>> u = UserFactory(username='leo') + >>> u.email + 'leo@example.com' + + +Decorator +~~~~~~~~~ + +.. function:: lazy_attribute + +If a simple lambda isn't enough, you may use the :meth:`lazy_attribute` decorator instead. + +This decorates an instance method that should take a single argument, ``self``; +the name of the method will be used as the name of the attribute to fill with the +return value of the method: + +.. code-block:: python + + class UserFactory(factory.Factory) + FACTORY_FOR = User + + name = u"Jean" + + @factory.lazy_attribute + def email(self): + # Convert to plain ascii text + clean_name = (unicodedata.normalize('NFKD', self.name) + .encode('ascii', 'ignore') + .decode('utf8')) + return u'%s@example.com' % clean_name + +.. code-block:: pycon + + >>> joel = UserFactory(name=u"Joël") + >>> joel.email + u'joel@example.com' + + +Sequence +"""""""" + +.. class:: Sequence(lambda, type=int) + +If a field should be unique, and thus different for all built instances, +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 is deprecated in 1.3.0 and will be removed in 2.0.0. + + +.. code-block:: python + + class UserFactory(factory.Factory) + FACTORY_FOR = User + + phone = factory.Sequence(lambda n: '123-555-%04d' % n) + +.. code-block:: pycon + + >>> UserFactory().phone + '123-555-0001' + >>> UserFactory().phone + '123-555-0002' + + +Decorator +~~~~~~~~~ + +.. function:: sequence + +As with :meth:`lazy_attribute`, a decorator is available for complex situations. + +:meth:`sequence` decorates an instance method, whose ``self`` method will actually +be the sequence counter - this might be confusing: + +.. code-block:: python + + class UserFactory(factory.Factory) + FACTORY_FOR = User + + @factory.sequence + def phone(n): + a = n // 10000 + b = n % 10000 + return '%03d-555-%04d' % (a, b) + +.. code-block:: pycon + + >>> UserFactory().phone + '000-555-9999' + >>> UserFactory().phone + '001-555-0000' + + +Sharing +~~~~~~~ + +The sequence counter is shared across all :class:`Sequence` attributes of the +:class:`Factory`: + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + phone = factory.Sequence(lambda n: '%04d' % n) + office = factory.Sequence(lambda n: 'A23-B%03d' % n) + +.. code-block:: pycon + + >>> u = UserFactory() + >>> u.phone, u.office + '0041', 'A23-B041' + >>> u2 = UserFactory() + >>> u2.phone, u2.office + '0042', 'A23-B042' + + +Inheritance +~~~~~~~~~~~ + +When a :class:`Factory` inherits from another :class:`Factory`, their +sequence counter is shared: + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + phone = factory.Sequence(lambda n: '123-555-%04d' % n) + + + class EmployeeFactory(UserFactory): + office_phone = factory.Sequence(lambda n: '%04d' % n) + +.. code-block:: pycon + + >>> u = UserFactory() + >>> u.phone + '123-555-0001' + + >>> e = EmployeeFactory() + >>> e.phone, e.office_phone + '123-555-0002', '0002' + + >>> u2 = UserFactory() + >>> u2.phone + '123-555-0003' + + +LazyAttributeSequence +""""""""""""""""""""" + +.. class:: LazyAttributeSequence(method_to_call) + +The :class:`LazyAttributeSequence` declaration merges features of :class:`Sequence` +and :class:`LazyAttribute`. + +It takes a single argument, a function whose two parameters are, in order: + +* The object being built +* The sequence counter + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + login = 'john' + email = factory.LazyAttributeSequence(lambda o, n: '%s@s%d.example.com' % (o.login, n)) + +.. code-block:: pycon + + >>> UserFactory().email + 'john@s1.example.com' + >>> UserFactory(login='jack').email + 'jack@s2.example.com' + + +Decorator +~~~~~~~~~ + +.. function:: lazy_attribute_sequence(method_to_call) + +As for :meth:`lazy_attribute` and :meth:`sequence`, the :meth:`lazy_attribute_sequence` +handles more complex cases: + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + login = 'john' + + @lazy_attribute_sequence + def email(self, n): + bucket = n % 10 + return '%s@s%d.example.com' % (self.login, bucket) + + +SubFactory +"""""""""" + +.. class:: SubFactory(sub_factory, **kwargs) + + .. OHAI_VIM** + +This attribute declaration calls another :class:`Factory` subclass, +selecting the same build strategy and collecting extra kwargs in the process. + +The :class:`SubFactory` attribute should be called with: + +* A :class:`Factory` subclass as first argument, or the fully qualified import + path to that :class:`Factory` (see :ref:`Circular imports `) +* An optional set of keyword arguments that should be passed when calling that + factory + + +Definition +~~~~~~~~~~ + +.. code-block:: python + + + # A standard factory + class UserFactory(factory.Factory): + FACTORY_FOR = User + + # Various fields + first_name = 'John' + last_name = factory.Sequence(lambda n: 'D%se' % ('o' * n)) # De, Doe, Dooe, Doooe, ... + email = factory.LazyAttribute(lambda o: '%s.%s@example.org' % (o.first_name.lower(), o.last_name.lower())) + + # A factory for an object with a 'User' field + class CompanyFactory(factory.Factory): + FACTORY_FOR = Company + + name = factory.Sequence(lambda n: 'FactoryBoyz' + 'z' * n) + + # Let's use our UserFactory to create that user, and override its first name. + owner = factory.SubFactory(UserFactory, first_name='Jack') + + +Calling +~~~~~~~ + +The wrapping factory will call of the inner factory: + +.. code-block:: pycon + + >>> c = CompanyFactory() + >>> c + + + # Notice that the first_name was overridden + >>> c.owner + + >>> c.owner.email + jack.de@example.org + + +Fields of the :class:`~factory.SubFactory` may be overridden from the external factory: + +.. code-block:: pycon + + >>> c = CompanyFactory(owner__first_name='Henry') + >>> c.owner + + + # Notice that the updated first_name was propagated to the email LazyAttribute. + >>> c.owner.email + henry.doe@example.org + + # It is also possible to override other fields of the SubFactory + >>> c = CompanyFactory(owner__last_name='Jones') + >>> c.owner + + >>> c.owner.email + henry.jones@example.org + + +Strategies +~~~~~~~~~~ + +The strategy chosen for the external factory will be propagated to all subfactories: + +.. code-block:: pycon + + >>> c = CompanyFactory() + >>> c.pk # Saved to the database + 3 + >>> c.owner.pk # Saved to the database + 8 + + >>> c = CompanyFactory.build() + >>> c.pk # Not saved + None + >>> c.owner.pk # Not saved either + None + + +.. _subfactory-circular: + +Circular imports +~~~~~~~~~~~~~~~~ + +Some factories may rely on each other in a circular manner. +This issue can be handled by passing the absolute import path to the target +:class:`Factory` to the :class:`SubFactory`: + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + username = 'john' + main_group = factory.SubFactory('users.factories.GroupFactory') + + class GroupFactory(factory.Factory): + FACTORY_FOR = Group + + name = "MyGroup" + owner = factory.SubFactory(UserFactory) + + +Obviously, such circular relationships require careful handling of loops: + +.. code-block:: pycon + + >>> owner = UserFactory(main_group=None) + >>> UserFactory(main_group__owner=owner) + + + +SelfAttribute +""""""""""""" + +.. class:: SelfAttribute(dotted_path_to_attribute) + +Some fields should reference another field of the object being constructed, or an attribute thereof. + +This is performed by the :class:`~factory.SelfAttribute` declaration. +That declaration takes a single argument, a dot-delimited path to the attribute to fetch: + +.. code-block:: python + + class UserFactory(factory.Factory) + FACTORY_FOR = User + + birthdate = factory.Sequence(lambda n: datetime.date(2000, 1, 1) + datetime.timedelta(days=n)) + birthmonth = factory.SelfAttribute('birthdate.month') + +.. code-block:: pycon + + >>> u = UserFactory() + >>> u.birthdate + date(2000, 3, 15) + >>> u.birthmonth + 3 + + +Parents +~~~~~~~ + +When used in conjunction with :class:`~factory.SubFactory`, the :class:`~factory.SelfAttribute` +gains an "upward" semantic through the double-dot notation, as used in Python imports. + +``factory.SelfAttribute('..country.language')`` means +"Select the ``language`` of the ``country`` of the :class:`~factory.Factory` calling me". + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + language = 'en' + + + class CompanyFactory(factory.Factory): + FACTORY_FOR = Company + + country = factory.SubFactory(CountryFactory) + owner = factory.SubFactory(UserFactory, language=factory.SelfAttribute('..country.language')) + +.. code-block:: pycon + + >>> company = CompanyFactory() + >>> company.country.language + 'fr' + >>> company.owner.language + 'fr' + +Obviously, this "follow parents" hability also handles overriding some attributes on call: + +.. code-block:: pycon + + >>> company = CompanyFactory(country=china) + >>> company.owner.language + 'cn' + + +Iterator +"""""""" + +.. class:: Iterator(iterable, cycle=True, getter=None) + +The :class:`Iterator` declaration takes succesive values from the given +iterable. When it is exhausted, it starts again from zero (unless ``cycle=False``). + +.. note:: Versions prior to 1.3.0 declared both :class:`Iterator` (for ``cycle=False``) + and :class:`InfiniteIterator` (for ``cycle=True``). + + :class:`InfiniteIterator` is deprecated as of 1.3.0 and will be removed in 2.0.0 + +The ``cycle`` argument is only useful for advanced cases, where the provided +iterable has no end (as wishing to cycle it means storing values in memory...). + +Each call to the factory will receive the next value from the iterable: + +.. code-block:: python + + class UserFactory(factory.Factory) + lang = factory.Iterator(['en', 'fr', 'es', 'it', 'de']) + +.. code-block:: pycon + + >>> UserFactory().lang + 'en' + >>> UserFactory().lang + 'fr' + + +When a value is passed in for the argument, the iterator will *not* be advanced: + +.. code-block:: pycon + + >>> UserFactory().lang + 'en' + >>> UserFactory(lang='cn').lang + 'cn' + >>> UserFactory().lang + 'fr' + +Getter +~~~~~~ + +Some situations may reuse an existing iterable, using only some component. +This is handled by the :attr:`~Iterator.getter` attribute: this is a function +that accepts as sole parameter a value from the iterable, and returns an +adequate value. + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + # CATEGORY_CHOICES is a list of (key, title) tuples + category = factory.Iterator(User.CATEGORY_CHOICES, getter=lambda c: c[0]) + + +post-building hooks +""""""""""""""""""" + +Some objects expect additional method calls or complex processing for proper definition. +For instance, a ``User`` may need to have a related ``Profile``, where the ``Profile`` is built from the ``User`` object. + +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. + + +RelatedFactory +"""""""""""""" + +.. class:: RelatedFactory(some_factory, related_field, **kwargs) + + .. OHAI_VIM** + +A :class:`RelatedFactory` behaves mostly like a :class:`SubFactory`, +with the main difference that it should be provided with a ``related_field`` name +as second argument. + +Once the base object has been built (or created), the :class:`RelatedFactory` will +build the :class:`Factory` passed as first argument (with the same strategy), +passing in the base object as a keyword argument whose name is passed in the +``related_field`` argument: + +.. code-block:: python + + class CityFactory(factory.Factory): + FACTORY_FOR = City + + capital_of = None + name = "Toronto" + + class CountryFactory(factory.Factory): + FACTORY_FOR = Country + + lang = 'fr' + capital_city = factory.RelatedFactory(CityFactory, 'capital_of', name="Paris") + +.. code-block:: pycon + + >>> france = CountryFactory() + >>> City.objects.get(capital_of=france) + + +Extra kwargs may be passed to the related factory, through the usual ``ATTR__SUBATTR`` syntax: + +.. code-block:: pycon + + >>> england = CountryFactory(lang='en', capital_city__name="London") + >>> City.objects.get(capital_of=england) + + + +PostGeneration +"""""""""""""" + +.. class:: PostGeneration(callable) + +The :class:`PostGeneration` declaration performs actions once the target object +has been generated. + +Its sole argument is a callable, that will be called once the base object has + been generated. + +.. note:: Previous versions of factory_boy supported an extra ``extract_prefix`` + argument, to use an alternate argument prefix. + This feature is deprecated in 1.3.0 and will be removed in 2.0.0. + +Once the base object has been generated, the provided callable will be called +as ``callable(obj, create, extracted, **kwargs)``, where: + +- ``obj`` is the base object previously generated +- ``create`` is a boolean indicating which strategy was used +- ``extracted`` is ``None`` unless a value was passed in for the + :class:`PostGeneration` declaration at :class:`Factory` declaration time +- ``kwargs`` are any extra parameters passed as ``attr__key=value`` when calling + the :class:`Factory`: + + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + login = 'john' + make_mbox = factory.PostGeneration( + lambda obj, create, extracted, **kwargs: os.makedirs(obj.login)) + +.. OHAI_VIM** + +Decorator +~~~~~~~~~ + +.. function:: post_generation(extract_prefix=None) + +A decorator is also provided, decorating a single method accepting the same +``obj``, ``created``, ``extracted`` and keyword arguments as :class:`PostGeneration`. + + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + login = 'john' + + @factory.post_generation + def mbox(self, create, extracted, **kwargs): + if not create: + return + path = extracted or os.path.join('/tmp/mbox/', self.login) + os.path.makedirs(path) + return path + +.. OHAI_VIM** + +.. code-block:: pycon + + >>> UserFactory.build() # Nothing was created + >>> UserFactory.create() # Creates dir /tmp/mbox/john + >>> UserFactory.create(login='jack') # Creates dir /tmp/mbox/jack + >>> UserFactory.create(mbox='/tmp/alt') # Creates dir /tmp/alt + + +PostGenerationMethodCall +"""""""""""""""""""""""" + +.. class:: PostGenerationMethodCall(method_name, extract_prefix=None, *args, **kwargs) + +.. OHAI_VIM* + +The :class:`PostGenerationMethodCall` declaration will call a method on the +generated object just after it being called. + +Its sole argument is the name of the method to call. +Extra arguments and keyword arguments for the target method may also be provided. + +Once the object has been generated, the method will be called, with the arguments +provided in the :class:`PostGenerationMethodCall` declaration, and keyword +arguments taken from the combination of :class:`PostGenerationMethodCall` +declaration and prefix-based values: + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + password = factory.PostGenerationMethodCall('set_password', password='') + +.. code-block:: pycon + + >>> UserFactory() # Calls user.set_password(password='') + >>> UserFactory(password='test') # Calls user.set_password(password='test') + >>> UserFactory(password__disabled=True) # Calls user.set_password(password='', disabled=True) + + +Module-level functions +---------------------- + +Beyond the :class:`Factory` class and the various :ref:`declarations` classes +and methods, factory_boy exposes a few module-level functions, mostly useful +for lightweight factory generation. + + +Lightweight factory declaration +""""""""""""""""""""""""""""""" + +.. function:: make_factory(klass, **kwargs) + + .. OHAI_VIM** + + The :func:`make_factory` function takes a class, declarations as keyword arguments, + and generates a new :class:`Factory` for that class accordingly: + + .. code-block:: python + + UserFactory = make_factory(User, + login='john', + email=factory.LazyAttribute(lambda u: '%s@example.com' % u.login), + ) + + # This is equivalent to: + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + login = 'john' + email = factory.LazyAttribute(lambda u: '%s@example.com' % u.login) + + +Instance building +""""""""""""""""" + +The :mod:`factory` module provides a bunch of shortcuts for creating a factory and +extracting instances from them: + +.. function:: build(klass, **kwargs) +.. function:: build_batch(klass, size, **kwargs) + + Create a factory for :obj:`klass` using declarations passed in kwargs; + return an instance built from that factory, + or a list of :obj:`size` instances (for :func:`build_batch`). + + :param class klass: Class of the instance to build + :param int size: Number of instances to build + :param kwargs: Declarations to use for the generated factory + + + +.. function:: create(klass, **kwargs) +.. function:: create_batch(klass, size, **kwargs) + + Create a factory for :obj:`klass` using declarations passed in kwargs; + return an instance created from that factory, + or a list of :obj:`size` instances (for :func:`create_batch`). + + :param class klass: Class of the instance to create + :param int size: Number of instances to create + :param kwargs: Declarations to use for the generated factory + + + +.. function:: stub(klass, **kwargs) +.. function:: stub_batch(klass, size, **kwargs) + + Create a factory for :obj:`klass` using declarations passed in kwargs; + return an instance stubbed from that factory, + or a list of :obj:`size` instances (for :func:`stub_batch`). + + :param class klass: Class of the instance to stub + :param int size: Number of instances to stub + :param kwargs: Declarations to use for the generated factory + + + +.. function:: generate(klass, strategy, **kwargs) +.. function:: generate_batch(klass, strategy, size, **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`). + + :param class 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 + + + +.. function:: simple_generate(klass, create, **kwargs) +.. function:: simple_generate_batch(klass, create, size, **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`). + + :param class 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 + + diff --git a/docs/subfactory.rst b/docs/subfactory.rst deleted file mode 100644 index f8642f3..0000000 --- a/docs/subfactory.rst +++ /dev/null @@ -1,81 +0,0 @@ -SubFactory -========== - -Some objects may use other complex objects as parameters; in order to simplify this setup, factory_boy -provides the :py:class:`factory.SubFactory` class. - -This should be used when defining a :py:class:`~factory.Factory` attribute that will hold the other complex object:: - - import factory - - # A standard factory - class UserFactory(factory.Factory): - FACTORY_FOR = User - - # Various fields - first_name = 'John' - last_name = factory.Sequence(lambda n: 'D%se' % ('o' * n)) # De, Doe, Dooe, Doooe, ... - email = factory.LazyAttribute(lambda o: '%s.%s@example.org' % (o.first_name.lower(), o.last_name.lower())) - - # A factory for an object with a 'User' field - class CompanyFactory(factory.Factory): - FACTORY_FOR = Company - - name = factory.Sequence(lambda n: 'FactoryBoyz' + z * n) - - # Let's use our UserFactory to create that user, and override its first name. - owner = factory.SubFactory(UserFactory, first_name='Jack') - -Instantiating the external factory will in turn instantiate an object of the internal factory:: - - >>> c = CompanyFactory() - >>> c - - - # Notice that the first_name was overridden - >>> c.owner - - >>> c.owner.email - jack.de@example.org - -Fields of the SubFactory can also be overridden when instantiating the external factory:: - - >>> c = CompanyFactory(owner__first_name='Henry') - >>> c.owner - - - # Notice that the updated first_name was propagated to the email LazyAttribute. - >>> c.owner.email - henry.doe@example.org - - # It is also possible to override other fields of the SubFactory - >>> c = CompanyFactory(owner__last_name='Jones') - >>> c.owner - - >>> c.owner.email - henry.jones@example.org - - -Circular dependencies ---------------------- - -In order to solve circular dependency issues, Factory Boy provides the :class:`~factory.CircularSubFactory` class. - -This class expects a module name and a factory name to import from that module; the given module will be imported -(as an absolute import) when the factory is first accessed:: - - # foo/factories.py - import factory - - from bar import factories - - class FooFactory(factory.Factory): - bar = factory.SubFactory(factories.BarFactory) - - - # bar/factories.py - import factory - - class BarFactory(factory.Factory): - # Avoid circular import - foo = factory.CircularSubFactory('foo.factories', 'FooFactory', bar=None) -- cgit v1.2.3 From 197e47a469b99595d992a51fbd48df0cd85532da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 13 Feb 2013 00:34:27 +0100 Subject: Add a Makefile. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- Makefile | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..144c2e9 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +all: default + + +default: + + +clean: + find . -type f -name '*.pyc' -delete + + +test: + python -W default setup.py test + +coverage: + coverage erase + coverage run "--include=factory/*.py,tests/*.py" --branch setup.py test + coverage report "--include=factory/*.py,tests/*.py" + coverage html "--include=factory/*.py,tests/*.py" + + +doc: + $(MAKE) -C docs html + + +.PHONY: all default clean coverage doc test -- cgit v1.2.3 From 050af55b77372b39fb6efecfb93bb6ee8ee425ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 3 Mar 2013 17:51:49 +0100 Subject: Improve testing helpers. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- Makefile | 1 - setup.py | 9 ++++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 144c2e9..f9c27c0 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,6 @@ coverage: coverage report "--include=factory/*.py,tests/*.py" coverage html "--include=factory/*.py,tests/*.py" - doc: $(MAKE) -C docs html diff --git a/setup.py b/setup.py index 57e701e..ef3da96 100755 --- a/setup.py +++ b/setup.py @@ -51,7 +51,14 @@ class test(cmd.Command): else: verbosity=0 - suite = unittest.TestLoader().loadTestsFromName(self.test_suite) + loader = unittest.TestLoader() + suite = unittest.TestSuite() + + if self.test_suite == 'tests': + for test_module in loader.discover('.'): + suite.addTest(test_module) + else: + suite.addTest(loader.loadTestsFromName(self.test_suite)) result = unittest.TextTestRunner(verbosity=verbosity).run(suite) if not result.wasSuccessful(): -- cgit v1.2.3 From ebdfb8cc5bab1e59b593a4ea60e55b9e7af455ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 1 Mar 2013 01:35:26 +0100 Subject: Improve Iterator and SubFactory declarations. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Iterator now cycles by default * Iterator can be provided with a custom getter * SubFactory accepts a factory import path as well Deprecates: * InfiniteIterator * CircularSubFactory Signed-off-by: Raphaël Barrois --- docs/reference.rst | 78 ++++++++++++++++++++++++++++++++++++++++++---- factory/declarations.py | 65 ++++++++++++++++++++++++++------------ tests/cyclic/bar.py | 2 +- tests/test_declarations.py | 72 ++++++++++++++++++++++++++++++++++++++++++ tests/test_using.py | 34 ++++++++++++++++++++ 5 files changed, 224 insertions(+), 27 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 289a9a8..edbd527 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -607,7 +607,9 @@ Circular imports Some factories may rely on each other in a circular manner. This issue can be handled by passing the absolute import path to the target -:class:`Factory` to the :class:`SubFactory`: +:class:`Factory` to the :class:`SubFactory`. + +.. versionadded:: 1.3.0 .. code-block:: python @@ -633,6 +635,19 @@ Obviously, such circular relationships require careful handling of loops: +.. class:: CircularSubFactory(module_name, symbol_name, **kwargs) + + .. OHAI_VIM** + + Lazily imports ``module_name.symbol_name`` at the first call. + +.. deprecated:: 1.3.0 + Merged into :class:`SubFactory`; will be removed in 2.0.0. + + Replace ``factory.CircularSubFactory('some.module', 'Symbol', **kwargs)`` + with ``factory.SubFactory('some.module.Symbol', **kwargs)`` + + SelfAttribute """"""""""""" @@ -708,14 +723,13 @@ Iterator The :class:`Iterator` declaration takes succesive values from the given iterable. When it is exhausted, it starts again from zero (unless ``cycle=False``). -.. note:: Versions prior to 1.3.0 declared both :class:`Iterator` (for ``cycle=False``) - and :class:`InfiniteIterator` (for ``cycle=True``). - - :class:`InfiniteIterator` is deprecated as of 1.3.0 and will be removed in 2.0.0 - The ``cycle`` argument is only useful for advanced cases, where the provided iterable has no end (as wishing to cycle it means storing values in memory...). +.. versionadded:: 1.3.0 + The ``cycle`` argument is available as of v1.3.0; previous versions + had a behaviour equivalent to ``cycle=False``. + Each call to the factory will receive the next value from the iterable: .. code-block:: python @@ -750,6 +764,8 @@ This is handled by the :attr:`~Iterator.getter` attribute: this is a function that accepts as sole parameter a value from the iterable, and returns an adequate value. +.. versionadded:: 1.3.0 + .. code-block:: python class UserFactory(factory.Factory): @@ -759,6 +775,56 @@ adequate value. category = factory.Iterator(User.CATEGORY_CHOICES, getter=lambda c: c[0]) +Decorator +~~~~~~~~~ + +.. function:: iterator(func) + + +When generating items of the iterator gets too complex for a simple list comprehension, +use the :func:`iterator` decorator: + +.. warning:: The decorated function takes **no** argument, + notably no ``self`` parameter. + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + @factory.iterator + def name(): + with open('test/data/names.dat', 'r') as f: + for line in f: + yield line + + +InfiniteIterator +~~~~~~~~~~~~~~~~ + +.. class:: InfiniteIterator(iterable) + + Equivalent to ``factory.Iterator(iterable)``. + +.. deprecated:: 1.3.0 + Merged into :class:`Iterator`; will be removed in v2.0.0. + + Replace ``factory.InfiniteIterator(iterable)`` + with ``factory.Iterator(iterable)``. + + +.. function:: infinite_iterator(function) + + Equivalent to ``factory.iterator(func)``. + + +.. deprecated:: 1.3.0 + Merged into :func:`iterator`; will be removed in v2.0.0. + + Replace ``@factory.infinite_iterator`` with ``@factory.iterator``. + + + post-building hooks """"""""""""""""""" diff --git a/factory/declarations.py b/factory/declarations.py index c64a0e5..d5b7950 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -22,6 +22,7 @@ import itertools +import warnings from factory import utils @@ -135,14 +136,23 @@ class Iterator(OrderedDeclaration): Attributes: iterator (iterable): the iterator whose value should be used. + getter (callable or None): a function to parse returned values """ - def __init__(self, iterator): + def __init__(self, iterator, cycle=True, getter=None): super(Iterator, self).__init__() - self.iterator = iter(iterator) + self.getter = getter + + if cycle: + self.iterator = itertools.cycle(iterator) + else: + self.iterator = iter(iterator) def evaluate(self, sequence, obj, containers=()): - return next(self.iterator) + value = next(self.iterator) + if self.getter is None: + return value + return self.getter(value) class InfiniteIterator(Iterator): @@ -153,7 +163,11 @@ class InfiniteIterator(Iterator): """ def __init__(self, iterator): - return super(InfiniteIterator, self).__init__(itertools.cycle(iterator)) + warnings.warn( + "factory.InfiniteIterator is deprecated, and will be removed in the " + "future. Please use factory.Iterator instead.", + PendingDeprecationWarning, 2) + return super(InfiniteIterator, self).__init__(iterator, cycle=True) class Sequence(OrderedDeclaration): @@ -289,10 +303,24 @@ class SubFactory(ParameteredAttribute): def __init__(self, factory, **kwargs): super(SubFactory, self).__init__(**kwargs) - self.factory = factory + if isinstance(factory, type): + self.factory = factory + self.factory_module = self.factory_name = '' + else: + # Must be a string + if not isinstance(factory, basestring) or '.' not in factory: + raise ValueError( + "The argument of a SubFactory must be either a class " + "or the fully qualified path to a Factory class; got " + "%r instead." % factory) + self.factory = None + self.factory_module, self.factory_name = factory.rsplit('.', 1) def get_factory(self): """Retrieve the wrapped factory.Factory subclass.""" + if self.factory is None: + # Must be a module path + self.factory = utils.import_object(self.factory_module, self.factory_name) return self.factory def generate(self, create, params): @@ -314,22 +342,15 @@ class SubFactory(ParameteredAttribute): class CircularSubFactory(SubFactory): """Use to solve circular dependencies issues.""" def __init__(self, module_name, factory_name, **kwargs): - super(CircularSubFactory, self).__init__(None, **kwargs) - self.module_name = module_name - self.factory_name = factory_name + factory = '%s.%s' % (module_name, factory_name) + warnings.warn( + "factory.CircularSubFactory is deprecated and will be removed in " + "the future. " + "Please replace factory.CircularSubFactory('module', 'symbol') " + "with factory.SubFactory('module.symbol').", + PendingDeprecationWarning, 2) - def get_factory(self): - """Retrieve the factory.Factory subclass. - - Its value is cached in the 'factory' attribute, and retrieved through - the factory_getter callable. - """ - if self.factory is None: - factory_class = utils.import_object( - self.module_name, self.factory_name) - - self.factory = factory_class - return self.factory + super(CircularSubFactory, self).__init__(factory, **kwargs) class PostGenerationDeclaration(object): @@ -456,6 +477,10 @@ def iterator(func): def infinite_iterator(func): """Turn a generator function into an infinite iterator attribute.""" + warnings.warn( + "@factory.infinite_iterator is deprecated and will be removed in the " + "future. Please use @factory.iterator instead.", + PendingDeprecationWarning, 2) return InfiniteIterator(func()) def sequence(func): diff --git a/tests/cyclic/bar.py b/tests/cyclic/bar.py index a8c9670..fed0602 100644 --- a/tests/cyclic/bar.py +++ b/tests/cyclic/bar.py @@ -33,5 +33,5 @@ class BarFactory(factory.Factory): FACTORY_FOR = Bar y = 13 - foo = factory.CircularSubFactory('cyclic.foo', 'FooFactory') + foo = factory.SubFactory('cyclic.foo.FooFactory') diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 214adc0..ecec244 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -21,10 +21,14 @@ # THE SOFTWARE. import datetime +import itertools +import warnings from factory import declarations from .compat import unittest +from . import tools + class OrderedDeclarationTestCase(unittest.TestCase): def test_errors(self): @@ -88,6 +92,28 @@ class SelfAttributeTestCase(unittest.TestCase): self.assertEqual(declarations._UNSPECIFIED, a.default) +class IteratorTestCase(unittest.TestCase): + def test_cycle(self): + it = declarations.Iterator([1, 2]) + self.assertEqual(1, it.evaluate(0, None)) + self.assertEqual(2, it.evaluate(1, None)) + self.assertEqual(1, it.evaluate(2, None)) + self.assertEqual(2, it.evaluate(3, None)) + + def test_no_cycling(self): + it = declarations.Iterator([1, 2], cycle=False) + self.assertEqual(1, it.evaluate(0, None)) + self.assertEqual(2, it.evaluate(1, None)) + self.assertRaises(StopIteration, it.evaluate, 2, None) + + def test_getter(self): + it = declarations.Iterator([(1, 2), (1, 3)], getter=lambda p: p[1]) + self.assertEqual(2, it.evaluate(0, None)) + self.assertEqual(3, it.evaluate(1, None)) + self.assertEqual(2, it.evaluate(2, None)) + self.assertEqual(3, it.evaluate(3, None)) + + class PostGenerationDeclarationTestCase(unittest.TestCase): def test_extract_no_prefix(self): decl = declarations.PostGenerationDeclaration() @@ -105,7 +131,51 @@ class PostGenerationDeclarationTestCase(unittest.TestCase): self.assertEqual(kwargs, {'baz': 1}) +class SubFactoryTestCase(unittest.TestCase): + def test_lazyness(self): + f = declarations.SubFactory('factory.declarations.Sequence', x=3) + self.assertEqual(None, f.factory) + + self.assertEqual({'x': 3}, f.defaults) + + factory_class = f.get_factory() + self.assertEqual(declarations.Sequence, factory_class) + + def test_cache(self): + orig_date = datetime.date + f = declarations.SubFactory('datetime.date') + self.assertEqual(None, f.factory) + + factory_class = f.get_factory() + self.assertEqual(orig_date, factory_class) + + try: + # Modify original value + datetime.date = None + # Repeat import + factory_class = f.get_factory() + self.assertEqual(orig_date, factory_class) + + finally: + # IMPORTANT: restore attribute. + datetime.date = orig_date + + class CircularSubFactoryTestCase(unittest.TestCase): + + def test_circularsubfactory_deprecated(self): + with warnings.catch_warnings(record=True) as w: + __warningregistry__.clear() + + warnings.simplefilter('always') + declarations.CircularSubFactory('datetime', 'date') + + self.assertEqual(1, len(w)) + self.assertIn('CircularSubFactory', str(w[0].message)) + self.assertIn('deprecated', str(w[0].message)) + + + @tools.disable_warnings def test_lazyness(self): f = declarations.CircularSubFactory('factory.declarations', 'Sequence', x=3) self.assertEqual(None, f.factory) @@ -115,6 +185,7 @@ class CircularSubFactoryTestCase(unittest.TestCase): factory_class = f.get_factory() self.assertEqual(declarations.Sequence, factory_class) + @tools.disable_warnings def test_cache(self): orig_date = datetime.date f = declarations.CircularSubFactory('datetime', 'date') @@ -134,5 +205,6 @@ class CircularSubFactoryTestCase(unittest.TestCase): # IMPORTANT: restore attribute. datetime.date = orig_date + if __name__ == '__main__': unittest.main() diff --git a/tests/test_using.py b/tests/test_using.py index fb0e8b0..fb8c207 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1076,6 +1076,21 @@ class IteratorTestCase(unittest.TestCase): for i, obj in enumerate(objs): self.assertEqual(i + 10, obj.one) + def test_infinite_iterator_deprecated(self): + with warnings.catch_warnings(record=True) as w: + __warningregistry__.clear() + + warnings.simplefilter('always') + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + + foo = factory.InfiniteIterator(range(5)) + + self.assertEqual(1, len(w)) + self.assertIn('InfiniteIterator', str(w[0].message)) + self.assertIn('deprecated', str(w[0].message)) + + @disable_warnings def test_infinite_iterator(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -1088,6 +1103,7 @@ class IteratorTestCase(unittest.TestCase): self.assertEqual(i % 5, obj.one) @unittest.skipUnless(is_python2, "Scope bleeding fixed in Python3+") + @disable_warnings def test_infinite_iterator_list_comprehension_scope_bleeding(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -1098,6 +1114,7 @@ class IteratorTestCase(unittest.TestCase): self.assertRaises(TypeError, TestObjectFactory.build) + @disable_warnings def test_infinite_iterator_list_comprehension_protected(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -1125,6 +1142,23 @@ class IteratorTestCase(unittest.TestCase): for i, obj in enumerate(objs): self.assertEqual(i + 10, obj.one) + def test_infinite_iterator_decorator_deprecated(self): + with warnings.catch_warnings(record=True) as w: + __warningregistry__.clear() + + warnings.simplefilter('always') + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + + @factory.infinite_iterator + def one(): + return range(5) + + self.assertEqual(1, len(w)) + self.assertIn('infinite_iterator', str(w[0].message)) + self.assertIn('deprecated', str(w[0].message)) + + @disable_warnings def test_infinite_iterator_decorator(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject -- cgit v1.2.3 From aa900c9f00d6777b57440e660e24df1a15f2f20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 3 Mar 2013 18:05:17 +0100 Subject: Tests: move disable_warnings to its own class. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- tests/test_using.py | 32 ++++++++++++-------------------- tests/tools.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 tests/tools.py diff --git a/tests/test_using.py b/tests/test_using.py index fb8c207..2bab8c0 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -29,6 +29,7 @@ import warnings import factory from .compat import is_python2, unittest +from . import tools class TestObject(object): @@ -73,15 +74,6 @@ class TestModel(FakeModel): pass -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 SimpleBuildTestCase(unittest.TestCase): """Tests the minimalist 'factory.build/create' functions.""" @@ -112,13 +104,13 @@ class SimpleBuildTestCase(unittest.TestCase): self.assertEqual(obj.three, 3) self.assertEqual(obj.four, None) - @disable_warnings + @tools.disable_warnings def test_create(self): obj = factory.create(FakeModel, foo='bar') self.assertEqual(obj.id, 2) self.assertEqual(obj.foo, 'bar') - @disable_warnings + @tools.disable_warnings def test_create_batch(self): objs = factory.create_batch(FakeModel, 4, foo='bar') @@ -149,7 +141,7 @@ class SimpleBuildTestCase(unittest.TestCase): self.assertEqual(obj.id, None) self.assertEqual(obj.foo, 'bar') - @disable_warnings + @tools.disable_warnings def test_generate_create(self): obj = factory.generate(FakeModel, factory.CREATE_STRATEGY, foo='bar') self.assertEqual(obj.id, 2) @@ -170,7 +162,7 @@ class SimpleBuildTestCase(unittest.TestCase): self.assertEqual(obj.id, None) self.assertEqual(obj.foo, 'bar') - @disable_warnings + @tools.disable_warnings def test_generate_batch_create(self): objs = factory.generate_batch(FakeModel, factory.CREATE_STRATEGY, 20, foo='bar') @@ -196,7 +188,7 @@ class SimpleBuildTestCase(unittest.TestCase): self.assertEqual(obj.id, None) self.assertEqual(obj.foo, 'bar') - @disable_warnings + @tools.disable_warnings def test_simple_generate_create(self): obj = factory.simple_generate(FakeModel, True, foo='bar') self.assertEqual(obj.id, 2) @@ -212,7 +204,7 @@ class SimpleBuildTestCase(unittest.TestCase): self.assertEqual(obj.id, None) self.assertEqual(obj.foo, 'bar') - @disable_warnings + @tools.disable_warnings def test_simple_generate_batch_create(self): objs = factory.simple_generate_batch(FakeModel, True, 20, foo='bar') @@ -668,7 +660,7 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual('three', obj.three) self.assertEqual('four', obj.four) - @disable_warnings + @tools.disable_warnings def testSetCreationFunction(self): def creation_function(class_to_create, **kwargs): return "This doesn't even return an instance of {0}".format(class_to_create.__name__) @@ -1090,7 +1082,7 @@ class IteratorTestCase(unittest.TestCase): self.assertIn('InfiniteIterator', str(w[0].message)) self.assertIn('deprecated', str(w[0].message)) - @disable_warnings + @tools.disable_warnings def test_infinite_iterator(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -1103,7 +1095,7 @@ class IteratorTestCase(unittest.TestCase): self.assertEqual(i % 5, obj.one) @unittest.skipUnless(is_python2, "Scope bleeding fixed in Python3+") - @disable_warnings + @tools.disable_warnings def test_infinite_iterator_list_comprehension_scope_bleeding(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -1114,7 +1106,7 @@ class IteratorTestCase(unittest.TestCase): self.assertRaises(TypeError, TestObjectFactory.build) - @disable_warnings + @tools.disable_warnings def test_infinite_iterator_list_comprehension_protected(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -1158,7 +1150,7 @@ class IteratorTestCase(unittest.TestCase): self.assertIn('infinite_iterator', str(w[0].message)) self.assertIn('deprecated', str(w[0].message)) - @disable_warnings + @tools.disable_warnings def test_infinite_iterator_decorator(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject diff --git a/tests/tools.py b/tests/tools.py new file mode 100644 index 0000000..571899b --- /dev/null +++ b/tests/tools.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Mark Sandstrom +# Copyright (c) 2011-2013 Raphaël Barrois +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +import functools +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 + + -- cgit v1.2.3 From aecf1b73d9c9d653220b6d8825d5eacd2cb6bd74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 3 Mar 2013 18:34:54 +0100 Subject: Tests: improve deprecation warning detection in test_base. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- tests/test_base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_base.py b/tests/test_base.py index ba88d3b..c16d536 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -209,6 +209,7 @@ class FactoryCreationTestCase(unittest.TestCase): pass self.assertEqual(1, len(w)) + self.assertIn('discovery', str(w[0].message)) self.assertIn('deprecated', str(w[0].message)) def testStub(self): -- cgit v1.2.3 From 37621846d541d7afcad52e6dba8281bd1146cf09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 2 Mar 2013 01:33:53 +0100 Subject: Deprecate the extract_prefix option to PostGeneration. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a new, call-less syntax for the @post_generation decorator. Signed-off-by: Raphaël Barrois --- factory/declarations.py | 25 ++++++++++-- tests/test_declarations.py | 96 +++++++++++++++++++++++++++++++++++++++++++++- tests/test_using.py | 21 +++++++++- 3 files changed, 136 insertions(+), 6 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index d5b7950..83f4d32 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -363,6 +363,11 @@ class PostGenerationDeclaration(object): """ def __init__(self, extract_prefix=None): + if extract_prefix: + warnings.warn( + "The extract_prefix argument to PostGeneration declarations " + "is deprecated and will be removed in the future.", + PendingDeprecationWarning, 3) self.extract_prefix = extract_prefix def extract(self, name, attrs): @@ -410,10 +415,22 @@ class PostGeneration(PostGenerationDeclaration): self.function(obj, create, extracted, **kwargs) -def post_generation(extract_prefix=None): - def decorator(fun): - return PostGeneration(fun, extract_prefix=extract_prefix) - return decorator +def post_generation(*args, **kwargs): + assert len(args) + len(kwargs) <= 1, "post_generation takes at most one argument." + if args and callable(args[0]): + # Called as @post_generation applied to a function + return PostGeneration(args[0]) + else: + warnings.warn( + "The @post_generation should now be applied directly to the " + "function, without parameters. The @post_generation() and " + "@post_generation(extract_prefix='xxx') syntaxes are deprecated " + "and will be removed in the future; use @post_generation instead.", + PendingDeprecationWarning, 2) + extract_prefix = kwargs.get('extract_prefix') + def decorator(fun): + return PostGeneration(fun, extract_prefix=extract_prefix) + return decorator class RelatedFactory(PostGenerationDeclaration): diff --git a/tests/test_declarations.py b/tests/test_declarations.py index ecec244..ef7cc0f 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -122,6 +122,7 @@ class PostGenerationDeclarationTestCase(unittest.TestCase): self.assertEqual(extracted, 13) self.assertEqual(kwargs, {'bar': 42}) + @tools.disable_warnings def test_extract_with_prefix(self): decl = declarations.PostGenerationDeclaration(extract_prefix='blah') @@ -130,6 +131,100 @@ class PostGenerationDeclarationTestCase(unittest.TestCase): self.assertEqual(extracted, 42) self.assertEqual(kwargs, {'baz': 1}) + def test_extract_prefix_deprecated(self): + with warnings.catch_warnings(record=True) as w: + __warningregistry__.clear() + + warnings.simplefilter('always') + declarations.PostGenerationDeclaration(extract_prefix='blah') + + self.assertEqual(1, len(w)) + self.assertIn('extract_prefix', str(w[0].message)) + self.assertIn('deprecated', str(w[0].message)) + + def test_decorator_simple(self): + call_params = [] + @declarations.post_generation + def foo(*args, **kwargs): + call_params.append(args) + call_params.append(kwargs) + + extracted, kwargs = foo.extract('foo', + {'foo': 13, 'foo__bar': 42, 'blah': 42, 'blah__baz': 1}) + self.assertEqual(13, extracted) + self.assertEqual({'bar': 42}, kwargs) + + # No value returned. + foo.call(None, False, extracted, **kwargs) + self.assertEqual(2, len(call_params)) + self.assertEqual((None, False, 13), call_params[0]) + self.assertEqual({'bar': 42}, call_params[1]) + + @tools.disable_warnings + def test_decorator_call_no_prefix(self): + call_params = [] + @declarations.post_generation() + def foo(*args, **kwargs): + call_params.append(args) + call_params.append(kwargs) + + extracted, kwargs = foo.extract('foo', + {'foo': 13, 'foo__bar': 42, 'blah': 42, 'blah__baz': 1}) + self.assertEqual(13, extracted) + self.assertEqual({'bar': 42}, kwargs) + + # No value returned. + foo.call(None, False, extracted, **kwargs) + self.assertEqual(2, len(call_params)) + self.assertEqual((None, False, 13), call_params[0]) + self.assertEqual({'bar': 42}, call_params[1]) + + @tools.disable_warnings + def test_decorator_extract_prefix(self): + call_params = [] + @declarations.post_generation(extract_prefix='blah') + def foo(*args, **kwargs): + call_params.append(args) + call_params.append(kwargs) + + extracted, kwargs = foo.extract('foo', + {'foo': 13, 'foo__bar': 42, 'blah': 42, 'blah__baz': 1}) + self.assertEqual(42, extracted) + self.assertEqual({'baz': 1}, kwargs) + + # No value returned. + foo.call(None, False, extracted, **kwargs) + self.assertEqual(2, len(call_params)) + self.assertEqual((None, False, 42), call_params[0]) + self.assertEqual({'baz': 1}, call_params[1]) + + def test_decorator_call_no_prefix_deprecated(self): + with warnings.catch_warnings(record=True) as w: + __warningregistry__.clear() + + warnings.simplefilter('always') + @declarations.post_generation() + def foo(*args, **kwargs): + pass + + self.assertEqual(1, len(w)) + self.assertIn('post_generation', str(w[0].message)) + self.assertIn('deprecated', str(w[0].message)) + + def test_decorator_call_with_prefix_deprecated(self): + with warnings.catch_warnings(record=True) as w: + __warningregistry__.clear() + + warnings.simplefilter('always') + @declarations.post_generation(extract_prefix='blah') + def foo(*args, **kwargs): + pass + + # 2 warnings: decorator with brackets, and extract_prefix. + self.assertEqual(2, len(w)) + self.assertIn('post_generation', str(w[0].message)) + self.assertIn('deprecated', str(w[0].message)) + class SubFactoryTestCase(unittest.TestCase): def test_lazyness(self): @@ -174,7 +269,6 @@ class CircularSubFactoryTestCase(unittest.TestCase): self.assertIn('CircularSubFactory', str(w[0].message)) self.assertIn('deprecated', str(w[0].message)) - @tools.disable_warnings def test_lazyness(self): f = declarations.CircularSubFactory('factory.declarations', 'Sequence', x=3) diff --git a/tests/test_using.py b/tests/test_using.py index 2bab8c0..fd8befb 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1168,6 +1168,25 @@ class IteratorTestCase(unittest.TestCase): class PostGenerationTestCase(unittest.TestCase): def test_post_generation(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + + one = 1 + + @factory.post_generation + def incr_one(self, _create, _increment): + self.one += 1 + + obj = TestObjectFactory.build() + self.assertEqual(2, obj.one) + self.assertFalse(hasattr(obj, 'incr_one')) + + obj = TestObjectFactory.build(one=2) + self.assertEqual(3, obj.one) + self.assertFalse(hasattr(obj, 'incr_one')) + + @tools.disable_warnings + def test_post_generation_calling(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -1191,7 +1210,7 @@ class PostGenerationTestCase(unittest.TestCase): one = 1 - @factory.post_generation() + @factory.post_generation def incr_one(self, _create, increment=1): self.one += increment -- cgit v1.2.3 From f248ebda408faee17f32c8f260bcf2d5df27b83d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 3 Mar 2013 20:32:42 +0100 Subject: Improve coverage. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- tests/test_declarations.py | 4 +++ tests/test_using.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/tests/test_declarations.py b/tests/test_declarations.py index ef7cc0f..c57e77d 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -227,6 +227,10 @@ class PostGenerationDeclarationTestCase(unittest.TestCase): class SubFactoryTestCase(unittest.TestCase): + + def test_arg(self): + self.assertRaises(ValueError, declarations.SubFactory, 'UnqualifiedSymbol') + def test_lazyness(self): f = declarations.SubFactory('factory.declarations.Sequence', x=3) self.assertEqual(None, f.factory) diff --git a/tests/test_using.py b/tests/test_using.py index fd8befb..20593f4 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -296,6 +296,24 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual('one43', test_object1.one) self.assertEqual('two43', test_object1.two) + def test_custom_create(self): + class TestModelFactory(factory.Factory): + FACTORY_FOR = TestModel + + two = 2 + + @classmethod + def _create(cls, target_class, *args, **kwargs): + obj = target_class.create(**kwargs) + obj.properly_created = True + return obj + + obj = TestModelFactory.create(one=1) + self.assertEqual(1, obj.one) + self.assertEqual(2, obj.two) + self.assertEqual(1, obj.id) + self.assertTrue(obj.properly_created) + def test_sequence_batch(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -660,6 +678,19 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual('three', obj.three) self.assertEqual('four', obj.four) + @tools.disable_warnings + def test_set_building_function(self): + def building_function(class_to_create, **kwargs): + return "This doesn't even return an instance of {0}".format(class_to_create.__name__) + + class TestModelFactory(FakeModelFactory): + FACTORY_FOR = TestModel + + TestModelFactory.set_building_function(building_function) + + test_object = TestModelFactory.build() + self.assertEqual(test_object, "This doesn't even return an instance of TestModel") + @tools.disable_warnings def testSetCreationFunction(self): def creation_function(class_to_create, **kwargs): @@ -1309,6 +1340,50 @@ class PostGenerationTestCase(unittest.TestCase): # RelatedFactory received "parent" object self.assertEqual(obj, obj.related.three) + def test_related_factory_no_name(self): + relateds = [] + class TestRelatedObject(object): + def __init__(self, obj=None, one=None, two=None): + relateds.append(self) + self.one = one + self.two = two + self.three = obj + + class TestRelatedObjectFactory(factory.Factory): + FACTORY_FOR = TestRelatedObject + one = 1 + two = factory.LazyAttribute(lambda o: o.one + 1) + + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + one = 3 + two = 2 + three = factory.RelatedFactory(TestRelatedObjectFactory) + + obj = TestObjectFactory.build() + # Normal fields + self.assertEqual(3, obj.one) + self.assertEqual(2, obj.two) + # RelatedFactory was built + self.assertIsNone(obj.three) + self.assertEqual(1, len(relateds)) + related = relateds[0] + self.assertEqual(1, related.one) + self.assertEqual(2, related.two) + self.assertIsNone(related.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.assertEqual(2, len(relateds)) + + related = relateds[1] + self.assertEqual(3, related.one) + self.assertEqual(4, related.two) + class CircularTestCase(unittest.TestCase): def test_example(self): -- cgit v1.2.3 From e8dc25b5db5b470a64cc6a89259d476269fcebb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 3 Mar 2013 20:59:58 +0100 Subject: Get rid of the FACTORY_ABSTRACT rename. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was just adding noise to an already complex release. Signed-off-by: Raphaël Barrois --- docs/changelog.rst | 2 -- factory/base.py | 15 +++------------ tests/test_base.py | 2 +- tests/test_using.py | 4 ++-- 4 files changed, 6 insertions(+), 17 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2e7aaea..1e6d45b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -39,8 +39,6 @@ The following features have been deprecated and will be removed in an upcoming r - Usage of :meth:`~factory.Factory.set_creation_function` and :meth:`~factory.Factory.set_building_function` are now deprecated - - The :attr:`~factory.Factory.ABSTRACT_FACTORY` attribute has been renamed to - :attr:`~factory.Factory.FACTORY_ABSTRACT`. - Implicit associated class discovery is no longer supported, you must set the :attr:`~factory.Factory.FACTORY_FOR` attribute on all :class:`~factory.Factory` subclasses diff --git a/factory/base.py b/factory/base.py index c8c7f06..28d7cdb 100644 --- a/factory/base.py +++ b/factory/base.py @@ -212,21 +212,12 @@ class FactoryMetaClass(BaseFactoryMetaClass): for construction of an associated class instance at a later time.""" parent_factories = get_factory_bases(bases) - if not parent_factories or attrs.get('ABSTRACT_FACTORY', False) \ - or attrs.get('FACTORY_ABSTRACT', False): + if not parent_factories or attrs.get('ABSTRACT_FACTORY', False): # If this isn't a subclass of Factory, or specifically declared # abstract, don't do anything special. if 'ABSTRACT_FACTORY' in attrs: - warnings.warn( - "The 'ABSTRACT_FACTORY' class attribute has been renamed " - "to 'FACTORY_ABSTRACT' for naming consistency, and will " - "be ignored in the future. Please upgrade class %s." % - class_name, DeprecationWarning, 2) attrs.pop('ABSTRACT_FACTORY') - if 'FACTORY_ABSTRACT' in attrs: - attrs.pop('FACTORY_ABSTRACT') - return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs) base = parent_factories[0] @@ -650,7 +641,7 @@ class DjangoModelFactory(Factory): handle those for non-numerical primary keys. """ - FACTORY_ABSTRACT = True + ABSTRACT_FACTORY = True @classmethod def _setup_next_sequence(cls): @@ -669,7 +660,7 @@ class DjangoModelFactory(Factory): class MogoFactory(Factory): """Factory for mogo objects.""" - FACTORY_ABSTRACT = True + ABSTRACT_FACTORY = True @classmethod def _build(cls, target_class, *args, **kwargs): diff --git a/tests/test_base.py b/tests/test_base.py index c16d536..6f16e8f 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -47,7 +47,7 @@ class FakeDjangoModel(object): self.id = None class FakeModelFactory(base.Factory): - FACTORY_ABSTRACT = True + ABSTRACT_FACTORY = True @classmethod def _create(cls, target_class, *args, **kwargs): diff --git a/tests/test_using.py b/tests/test_using.py index 20593f4..e5af8fb 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -63,7 +63,7 @@ class FakeModel(object): class FakeModelFactory(factory.Factory): - FACTORY_ABSTRACT = True + ABSTRACT_FACTORY = True @classmethod def _create(cls, target_class, *args, **kwargs): @@ -253,7 +253,7 @@ class UsingFactoryTestCase(unittest.TestCase): def test_abstract(self): class SomeAbstractFactory(factory.Factory): - FACTORY_ABSTRACT = True + ABSTRACT_FACTORY = True one = 'one' class InheritedFactory(SomeAbstractFactory): -- cgit v1.2.3 From 94a7e2659e3e8b6a9183b59aed06223cc1706c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 3 Mar 2013 21:00:11 +0100 Subject: Update ChangeLog for 1.3.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- docs/changelog.rst | 65 +++++++++++++++++++++++++++++++++++++++++++++++++----- docs/reference.rst | 27 +++++++++++++++-------- 2 files changed, 77 insertions(+), 15 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1e6d45b..eccc0a6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,24 +24,77 @@ ChangeLog 1.3.0 (current) --------------- -*New:* +.. warning:: This version deprecates many magic or unexplicit features that will be + removed in v2.0.0. - - Add :class:`~factory.CircularSubFactory` to solve circular dependencies between factories + Please read the :ref:`changelog-1-3-0-upgrading` section, then run your + tests with ``python -W default`` to see all remaining warnings. + +New +""" + +- **Global:** + - Rewrite the whole documentation + - Provide a dedicated :class:`~factory.MogoFactory` subclass of :class:`~factory.Factory` + +- **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 - - Enhance :class:`~factory.SelfAttribute` to handle "container" attribute fetching + - 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 - - Provide a dedicated :class:`~factory.MogoFactory` subclass of :class:`~factory.Factory` -*Pending deprecation:* +- **Declarations:** + - Allow :class:`~factory.SubFactory` to solve circular dependencies between factories + - Enhance :class:`~factory.SelfAttribute` to handle "container" attribute fetching + - Add a :attr:`~factory.Iterator.getter` to :class:`~factory.Iterator` + declarations + - A :class:`~factory.Iterator` may be prevented from cycling by setting + its :attr:`~factory.Iterator.cycle` argument to ``False`` + + +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` + - 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` are now deprecated - Implicit associated class discovery is no longer supported, you must set the :attr:`~factory.Factory.FACTORY_FOR` attribute on all :class:`~factory.Factory` subclasses + +.. _changelog-1-3-0-upgrading: + +Upgrading +""""""""" + +This version deprecates a few magic or undocumented features. +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 + discovery +- When using factory_boy for Django models, have each factory inherit from + :class:`~factory.DjangoModelFactory` +- Replace ``factory.CircularSubFactory('some.module', 'Symbol')`` with + ``factory.SubFactory('some.module.Symbol')`` +- Replace ``factory.InfiniteIterator(iterable)`` with ``factory.Iterator(iterable)`` +- Replace ``@factory.post_generation()`` with ``@factory.post_generation`` +- Replace ``factory.set_building_function(SomeFactory, building_function)`` with + an override of the :meth:`~factory.Factory._build` method of ``SomeFactory`` +- Replace ``factory.set_creation_function(SomeFactory, creation_function)`` with + an override of the :meth:`~factory.Factory._create` method of ``SomeFactory`` + + + 1.2.0 (09/08/2012) ------------------ diff --git a/docs/reference.rst b/docs/reference.rst index edbd527..efb70e8 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -720,15 +720,24 @@ Iterator .. class:: Iterator(iterable, cycle=True, getter=None) -The :class:`Iterator` declaration takes succesive values from the given -iterable. When it is exhausted, it starts again from zero (unless ``cycle=False``). + The :class:`Iterator` declaration takes succesive values from the given + iterable. When it is exhausted, it starts again from zero (unless ``cycle=False``). -The ``cycle`` argument is only useful for advanced cases, where the provided -iterable has no end (as wishing to cycle it means storing values in memory...). + .. attribute:: cycle -.. versionadded:: 1.3.0 - The ``cycle`` argument is available as of v1.3.0; previous versions - had a behaviour equivalent to ``cycle=False``. + The ``cycle`` argument is only useful for advanced cases, where the provided + iterable has no end (as wishing to cycle it means storing values in memory...). + + .. versionadded:: 1.3.0 + The ``cycle`` argument is available as of v1.3.0; previous versions + had a behaviour equivalent to ``cycle=False``. + + .. attribute:: getter + + A custom function called on each value returned by the iterable. + See the :ref:`iterator-getter` section for details. + + .. versionadded:: 1.3.0 Each call to the factory will receive the next value from the iterable: @@ -756,6 +765,8 @@ When a value is passed in for the argument, the iterator will *not* be advanced: >>> UserFactory().lang 'fr' +.. _iterator-getter: + Getter ~~~~~~ @@ -764,8 +775,6 @@ This is handled by the :attr:`~Iterator.getter` attribute: this is a function that accepts as sole parameter a value from the iterable, and returns an adequate value. -.. versionadded:: 1.3.0 - .. code-block:: python class UserFactory(factory.Factory): -- cgit v1.2.3 From d220884ee474461af6a9704eb34cd91b0b99ea20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 3 Mar 2013 21:05:31 +0100 Subject: Tests: run tox tests with warnings enabled. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 944070f..204bde9 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py26,py27,pypy [testenv] commands= - python setup.py test + python -W default setup.py test [testenv:py26] -- cgit v1.2.3 From ef1b26e69398c3aa3a7657b2cb6a667316e68e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 3 Mar 2013 21:41:40 +0100 Subject: Improve links in README. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- README | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README b/README index a819972..0f20d84 100644 --- a/README +++ b/README @@ -14,7 +14,13 @@ Its features include: - Multiple factories per class support, including inheritance - Support for various ORMs (currently Django, Mogo) -The official repository is at http://github.com/rbarrois/factory_boy; the documentation at http://readthedocs.org/docs/factoryboy/. + +Links +----- + +* Documentation: http://factoryboy.readthedocs.org/ +* Official repository: https://github.com/rbarrois/factory_boy +* Package: https://pypi.python.org/pypi/factory_boy/ factory_boy supports Python 2.6 and 2.7 (Python 3 is in the works), and requires only the standard Python library. @@ -22,13 +28,13 @@ factory_boy supports Python 2.6 and 2.7 (Python 3 is in the works), and requires Download -------- -PyPI: http://pypi.python.org/pypi/factory_boy/ +PyPI: https://pypi.python.org/pypi/factory_boy/ .. code-block:: sh $ pip install factory_boy -Source: http://github.com/rbarrois/factory_boy/ +Source: https://github.com/rbarrois/factory_boy/ .. code-block:: sh -- cgit v1.2.3 From f8708d936be1aa53a8b61f95cda6edcdbd8fc00a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 3 Mar 2013 21:55:32 +0100 Subject: doc: Add recipe for SelfAttribute('..X'). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- docs/recipes.rst | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index 8af0c8f..b148cd5 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -25,7 +25,7 @@ use the :class:`~factory.SubFactory` declaration: import factory from . import models - class UserFactory(factory.Factory): + class UserFactory(factory.DjangoModelFactory): FACTORY_FOR = models.User first_name = factory.Sequence(lambda n: "Agent %03d" % n) @@ -52,7 +52,7 @@ use a :class:`~factory.RelatedFactory` declaration: # factories.py - class UserFactory(factory.Factory): + class UserFactory(factory.DjangoModelFactory): FACTORY_FOR = models.User log = factory.RelatedFactory(UserLogFactory, 'user', action=models.UserLog.ACTION_CREATE) @@ -60,3 +60,56 @@ use a :class:`~factory.RelatedFactory` declaration: When a :class:`UserFactory` is instantiated, factory_boy will call ``UserLogFactory(user=that_user, action=...)`` just before returning the created ``User``. + + +Copying fields to a SubFactory +------------------------------ + +When a field of a related class should match one of the container: + + +.. code-block:: python + + # models.py + class Country(models.Model): + name = models.CharField() + lang = models.CharField() + + class User(models.Model): + name = models.CharField() + lang = models.CharField() + country = models.ForeignKey(Country) + + class Company(models.Model): + name = models.CharField() + owner = models.ForeignKey(User) + country = models.ForeignKey(Country) + + +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')``) + +.. code-block:: python + + # factories.py + class CountryFactory(factory.DjangoModelFactory): + FACTORY_FOR = models.Country + + name = factory.Iterator(["France", "Italy", "Spain"]) + lang = factory.Iterator(['fr', 'it', 'es']) + + class UserFactory(factory.DjangoModelFactory): + FACTORY_FOR = models.User + + name = "John" + lang = factory.SelfAttribute('country.lang') + country = factory.SubFactory(CountryFactory) + + class CompanyFactory(factory.DjangoModelFactory): + FACTORY_FOR = models.Company + + name = "ACME, Inc." + country = factory.SubFactory(CountryFactory) + owner = factory.SubFactory(UserFactory, country=factory.SelfAttribute('..country)) -- cgit v1.2.3 From 9422cf12516143650f1014f34f996260c00d4c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 3 Mar 2013 22:10:42 +0100 Subject: Allow symbol names in RelatedFactory (Closes #30). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This works exactly as for SubFactory. Signed-off-by: Raphaël Barrois --- docs/reference.rst | 33 ++++++++++++++++++++++++--------- factory/declarations.py | 25 +++++++++++++++++++++++-- tests/test_declarations.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 11 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index efb70e8..e2246aa 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -215,7 +215,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. object, then save it: .. code-block:: pycon - + >>> obj = self._associated_class(*args, **kwargs) >>> obj.save() >>> return obj @@ -849,18 +849,32 @@ To support this pattern, factory_boy provides the following tools: RelatedFactory """""""""""""" -.. class:: RelatedFactory(some_factory, related_field, **kwargs) +.. class:: RelatedFactory(factory, name='', **kwargs) .. OHAI_VIM** -A :class:`RelatedFactory` behaves mostly like a :class:`SubFactory`, -with the main difference that it should be provided with a ``related_field`` name -as second argument. + A :class:`RelatedFactory` behaves mostly like a :class:`SubFactory`, + with the main difference that the related :class:`Factory` will be generated + *after* the base :class:`Factory`. + + + .. 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 factory if the :attr:`name` parameter + is set. + + It will be passed as a keyword argument, using the :attr:`name` value as + keyword: -Once the base object has been built (or created), the :class:`RelatedFactory` will -build the :class:`Factory` passed as first argument (with the same strategy), -passing in the base object as a keyword argument whose name is passed in the -``related_field`` argument: .. code-block:: python @@ -882,6 +896,7 @@ passing in the base object as a keyword argument whose name is passed in the >>> City.objects.get(capital_of=france) + Extra kwargs may be passed to the related factory, through the usual ``ATTR__SUBATTR`` syntax: .. code-block:: pycon diff --git a/factory/declarations.py b/factory/declarations.py index 83f4d32..d3d7659 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -445,16 +445,37 @@ class RelatedFactory(PostGenerationDeclaration): def __init__(self, factory, name='', **defaults): super(RelatedFactory, self).__init__(extract_prefix=None) - self.factory = factory self.name = name self.defaults = defaults + if isinstance(factory, type): + self.factory = factory + self.factory_module = self.factory_name = '' + else: + # Must be a string + if not isinstance(factory, basestring) or '.' not in factory: + raise ValueError( + "The argument of a SubFactory must be either a class " + "or the fully qualified path to a Factory class; got " + "%r instead." % factory) + self.factory = None + self.factory_module, self.factory_name = factory.rsplit('.', 1) + + def get_factory(self): + """Retrieve the wrapped factory.Factory subclass.""" + if self.factory is None: + # Must be a module path + self.factory = utils.import_object(self.factory_module, self.factory_name) + return self.factory + def call(self, obj, create, extracted=None, **kwargs): passed_kwargs = dict(self.defaults) passed_kwargs.update(kwargs) if self.name: passed_kwargs[self.name] = obj - self.factory.simple_generate(create, **passed_kwargs) + + factory = self.get_factory() + factory.simple_generate(create, **passed_kwargs) class PostGenerationMethodCall(PostGenerationDeclaration): diff --git a/tests/test_declarations.py b/tests/test_declarations.py index c57e77d..cc921d4 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -260,6 +260,41 @@ class SubFactoryTestCase(unittest.TestCase): datetime.date = orig_date +class RelatedFactoryTestCase(unittest.TestCase): + + def test_arg(self): + self.assertRaises(ValueError, declarations.RelatedFactory, 'UnqualifiedSymbol') + + def test_lazyness(self): + f = declarations.RelatedFactory('factory.declarations.Sequence', x=3) + self.assertEqual(None, f.factory) + + self.assertEqual({'x': 3}, f.defaults) + + factory_class = f.get_factory() + self.assertEqual(declarations.Sequence, factory_class) + + def test_cache(self): + """Ensure that RelatedFactory tries to import only once.""" + orig_date = datetime.date + f = declarations.RelatedFactory('datetime.date') + self.assertEqual(None, f.factory) + + factory_class = f.get_factory() + self.assertEqual(orig_date, factory_class) + + try: + # Modify original value + datetime.date = None + # Repeat import + factory_class = f.get_factory() + self.assertEqual(orig_date, factory_class) + + finally: + # IMPORTANT: restore attribute. + datetime.date = orig_date + + class CircularSubFactoryTestCase(unittest.TestCase): def test_circularsubfactory_deprecated(self): -- cgit v1.2.3 From 7d792430e103984a91c102c33da79be2426bc632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 4 Mar 2013 23:18:02 +0100 Subject: Add a 'after post_generation' hook to Factory. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use it in DjangoModelFactory to save objects again if a post_generation hook ran. Signed-off-by: Raphaël Barrois --- docs/orms.rst | 5 +++-- docs/reference.rst | 13 +++++++++++++ factory/base.py | 24 +++++++++++++++++++++++- factory/declarations.py | 2 +- tests/test_using.py | 21 +++++++++++++++++++++ 5 files changed, 61 insertions(+), 4 deletions(-) diff --git a/docs/orms.rst b/docs/orms.rst index eae31d9..d6ff3c3 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -32,5 +32,6 @@ All factories for a Django :class:`~django.db.models.Model` should use the * :func:`~Factory.create()` uses :meth:`Model.objects.create() ` * :func:`~Factory._setup_next_sequence()` selects the next unused primary key value - * When using :class:`~factory.RelatedFactory` attributes, the base object will be - :meth:`saved ` once all post-generation hooks have run. + * When using :class:`~factory.RelatedFactory` or :class:`~factory.PostGeneration` + attributes, the base object will be :meth:`saved ` + once all post-generation hooks have run. diff --git a/docs/reference.rst b/docs/reference.rst index e2246aa..d100b40 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -189,6 +189,19 @@ The :class:`Factory` class .. OHAI_VIM* + .. classmethod:: _after_postgeneration(cls, obj, create, results=None) + + :arg object obj: The object just generated + :arg bool create: Whether the object was 'built' or 'created' + :arg dict results: Map of post-generation declaration name to call + result + + The :meth:`_after_postgeneration` is called once post-generation + declarations have been handled. + + Its arguments allow to handle specifically some post-generation return + values, for instance. + .. _strategies: diff --git a/factory/base.py b/factory/base.py index 28d7cdb..3ebc746 100644 --- a/factory/base.py +++ b/factory/base.py @@ -616,11 +616,26 @@ class Factory(BaseFactory): obj = cls._prepare(create, **attrs) # Handle post-generation attributes + results = {} for name, decl in sorted(postgen_declarations.items()): extracted, extracted_kwargs = postgen_attributes[name] - decl.call(obj, create, extracted, **extracted_kwargs) + results[name] = decl.call(obj, create, extracted, **extracted_kwargs) + + cls._after_postgeneration(obj, create, results) + return obj + @classmethod + def _after_postgeneration(cls, obj, create, results=None): + """Hook called after post-generation declarations have been handled. + + Args: + obj (object): the generated object + create (bool): whether the strategy was 'build' or 'create' + results (dict or None): result of post-generation declarations + """ + pass + @classmethod def build(cls, **kwargs): attrs = cls.attributes(create=False, extra=kwargs) @@ -657,6 +672,13 @@ class DjangoModelFactory(Factory): """Create an instance of the model, and save it to the database.""" return target_class._default_manager.create(*args, **kwargs) + @classmethod + def _after_postgeneration(cls, obj, create, results=None): + """Save again the instance if creating and at least one hook ran.""" + if create and results: + # Some post-generation hooks ran, and may have modified us. + obj.save() + class MogoFactory(Factory): """Factory for mogo objects.""" diff --git a/factory/declarations.py b/factory/declarations.py index d3d7659..366c2c8 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -412,7 +412,7 @@ class PostGeneration(PostGenerationDeclaration): self.function = function def call(self, obj, create, extracted=None, **kwargs): - self.function(obj, create, extracted, **kwargs) + return self.function(obj, create, extracted, **kwargs) def post_generation(*args, **kwargs): diff --git a/tests/test_using.py b/tests/test_using.py index e5af8fb..9bc466e 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1216,6 +1216,27 @@ class PostGenerationTestCase(unittest.TestCase): self.assertEqual(3, obj.one) self.assertFalse(hasattr(obj, 'incr_one')) + def test_post_generation_hook(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + + one = 1 + + @factory.post_generation + def incr_one(self, _create, _increment): + self.one += 1 + return 42 + + @classmethod + def _after_postgeneration(cls, obj, create, results): + obj.create = create + obj.results = results + + obj = TestObjectFactory.build() + self.assertEqual(2, obj.one) + self.assertFalse(obj.create) + self.assertEqual({'incr_one': 42}, obj.results) + @tools.disable_warnings def test_post_generation_calling(self): class TestObjectFactory(factory.Factory): -- cgit v1.2.3 From be403fd5a109af49d228ab620ab14d04cb9e34c8 Mon Sep 17 00:00:00 2001 From: Chris Lasher Date: Thu, 17 Jan 2013 16:29:14 -0500 Subject: Use extracted argument in PostGenerationMethodCall. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This changeset makes it possible possible to override the default method arguments (or "method_args") passed in when instantiating PostGenerationMethodCall. Now the user can override the default arguments to the method called during post-generation when instantiating a factory. For example, using this UserFactory, class UserFactory(factory.Factory): FACTORY_FOR = User username = factory.Sequence(lambda n: 'user{0}'.format(n)) password = factory.PostGenerationMethodCall( 'set_password', None, 'defaultpassword') by default, the user will have a password set to 'defaultpassword', but this can be overridden by passing in a new password as a keyword argument: >>> u = UserFactory() >>> u.check_password('defaultpassword') True >>> other_u = UserFactory(password='different') >>> other_u.check_password('defaultpassword') False >>> other_u.check_password('different') True This changeset introduces a testing dependency on the Mock package http://pypi.python.org/pypi/mock. While this is a third-party dependency in Python 2, it is part of the Python 3 standard library, as unit.mock, and so a reasonable dependency to satisfy. Signed-off-by: Raphaël Barrois --- factory/declarations.py | 10 +++++++++- tests/test_declarations.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/factory/declarations.py b/factory/declarations.py index 366c2c8..1f1d2af 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -21,6 +21,7 @@ # THE SOFTWARE. +import collections import itertools import warnings @@ -498,10 +499,17 @@ class PostGenerationMethodCall(PostGenerationDeclaration): self.method_kwargs = kwargs def call(self, obj, create, extracted=None, **kwargs): + if extracted is not None: + passed_args = extracted + if isinstance(passed_args, basestring) or ( + not isinstance(passed_args, collections.Iterable)): + passed_args = (passed_args,) + else: + passed_args = self.method_args passed_kwargs = dict(self.method_kwargs) passed_kwargs.update(kwargs) method = getattr(obj, self.method_name) - method(*self.method_args, **passed_kwargs) + method(*passed_args, **passed_kwargs) # Decorators... in case lambdas don't cut it diff --git a/tests/test_declarations.py b/tests/test_declarations.py index cc921d4..59a3955 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -24,6 +24,8 @@ import datetime import itertools import warnings +from mock import MagicMock + from factory import declarations from .compat import unittest @@ -295,6 +297,51 @@ class RelatedFactoryTestCase(unittest.TestCase): datetime.date = orig_date +class PostGenerationMethodCallTestCase(unittest.TestCase): + def setUp(self): + self.obj = MagicMock() + + def test_simplest_setup_and_call(self): + decl = declarations.PostGenerationMethodCall('method') + decl.call(self.obj, False) + self.obj.method.assert_called_once_with() + + def test_call_with_method_args(self): + decl = declarations.PostGenerationMethodCall( + 'method', None, 'data') + decl.call(self.obj, False) + self.obj.method.assert_called_once_with('data') + + def test_call_with_passed_extracted_string(self): + decl = declarations.PostGenerationMethodCall( + 'method', None) + decl.call(self.obj, False, 'data') + self.obj.method.assert_called_once_with('data') + + def test_call_with_passed_extracted_int(self): + decl = declarations.PostGenerationMethodCall('method') + decl.call(self.obj, False, 1) + self.obj.method.assert_called_once_with(1) + + def test_call_with_passed_extracted_iterable(self): + decl = declarations.PostGenerationMethodCall('method') + decl.call(self.obj, False, (1, 2, 3)) + self.obj.method.assert_called_once_with(1, 2, 3) + + def test_call_with_method_kwargs(self): + decl = declarations.PostGenerationMethodCall( + 'method', None, data='data') + decl.call(self.obj, False) + self.obj.method.assert_called_once_with(data='data') + + def test_call_with_passed_kwargs(self): + decl = declarations.PostGenerationMethodCall('method') + decl.call(self.obj, False, data='other') + self.obj.method.assert_called_once_with(data='other') + + + + class CircularSubFactoryTestCase(unittest.TestCase): def test_circularsubfactory_deprecated(self): -- cgit v1.2.3 From 3c011a3c6e97e40410ad88a734605759fb247301 Mon Sep 17 00:00:00 2001 From: Chris Lasher Date: Fri, 18 Jan 2013 14:36:18 -0500 Subject: Let mock source be chosen by Python major version. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should provide better Python 3 compatibility, since mock is in the Python 3 standard library as unittest.mock. Conflicts: tests/test_declarations.py Signed-off-by: Raphaël Barrois --- tests/compat.py | 6 ++++++ tests/test_declarations.py | 6 ++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/compat.py b/tests/compat.py index 8d4f1d0..769ffd4 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -29,3 +29,9 @@ try: import unittest2 as unittest except ImportError: import unittest + +if is_python2: + import mock +else: + from unittest import mock + diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 59a3955..93e11d0 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -24,11 +24,9 @@ import datetime import itertools import warnings -from mock import MagicMock - from factory import declarations -from .compat import unittest +from .compat import mock, unittest from . import tools @@ -299,7 +297,7 @@ class RelatedFactoryTestCase(unittest.TestCase): class PostGenerationMethodCallTestCase(unittest.TestCase): def setUp(self): - self.obj = MagicMock() + self.obj = mock.MagicMock() def test_simplest_setup_and_call(self): decl = declarations.PostGenerationMethodCall('method') -- cgit v1.2.3 From 2bc0fc8413c02a7faf3a116fe875d76bc3403117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 5 Mar 2013 00:36:08 +0100 Subject: Cleanup argument extraction in PostGenMethod (See #36). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This provides a consistent behaviour for extracting arguments to a PostGenerationMethodCall. Signed-off-by: Raphaël Barrois --- docs/changelog.rst | 4 ++++ docs/reference.rst | 52 +++++++++++++++++++++++++++++++++++++++++----- factory/declarations.py | 17 ++++++++------- tests/test_declarations.py | 24 +++++++++++++++++---- 4 files changed, 81 insertions(+), 16 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index eccc0a6..0671f8a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -50,6 +50,10 @@ New declarations - A :class:`~factory.Iterator` may be prevented from cycling by setting its :attr:`~factory.Iterator.cycle` argument to ``False`` + - Allow overriding default arguments in a :class:`~factory.PostGenerationMethodCall` + when generating an instance of the factory + - An object created by a :class:`~factory.DjangoModelFactory` will be saved + again after :class:`~factory.PostGeneration` hooks execution Pending deprecation diff --git a/docs/reference.rst b/docs/reference.rst index d100b40..85b299c 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1003,10 +1003,22 @@ generated object just after it being called. Its sole argument is the name of the method to call. Extra arguments and keyword arguments for the target method may also be provided. -Once the object has been generated, the method will be called, with the arguments -provided in the :class:`PostGenerationMethodCall` declaration, and keyword -arguments taken from the combination of :class:`PostGenerationMethodCall` -declaration and prefix-based values: +Once the object has been generated, the method will be called, with arguments +taken from either the :class:`PostGenerationMethodCall` or prefix-based values: + +- If a value was extracted from kwargs (i.e an argument for the name the + :class:`PostGenerationMethodCall` was declared under): + + - If the declaration mentionned zero or one argument, the value is passed + directly to the method + - If the declaration used two or more arguments, the value is passed as + ``*args`` to the method + +- Otherwise, the arguments used when declaring the :class:`PostGenerationMethodCall` + are used + +- Keywords extracted from the factory arguments are merged into the defaults + present in the :class:`PostGenerationMethodCall` declaration. .. code-block:: python @@ -1018,10 +1030,40 @@ declaration and prefix-based values: .. code-block:: pycon >>> UserFactory() # Calls user.set_password(password='') - >>> UserFactory(password='test') # Calls user.set_password(password='test') + >>> UserFactory(password='test') # Calls user.set_password('test') >>> UserFactory(password__disabled=True) # Calls user.set_password(password='', disabled=True) +When the :class:`PostGenerationMethodCall` declaration uses two or more arguments, +the extracted value must be iterable: + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + password = factory.PostGenerationMethodCall('set_password', '', 'sha1') + +.. 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')``) + + Module-level functions ---------------------- diff --git a/factory/declarations.py b/factory/declarations.py index 1f1d2af..efaadbe 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -492,20 +492,23 @@ class PostGenerationMethodCall(PostGenerationDeclaration): ... password = factory.PostGenerationMethodCall('set_password', password='') """ - def __init__(self, method_name, extract_prefix=None, *args, **kwargs): + def __init__(self, method_name, *args, **kwargs): + extract_prefix = kwargs.pop('extract_prefix', None) super(PostGenerationMethodCall, self).__init__(extract_prefix) self.method_name = method_name self.method_args = args self.method_kwargs = kwargs def call(self, obj, create, extracted=None, **kwargs): - if extracted is not None: - passed_args = extracted - if isinstance(passed_args, basestring) or ( - not isinstance(passed_args, collections.Iterable)): - passed_args = (passed_args,) - else: + if extracted is None: passed_args = self.method_args + + elif len(self.method_args) <= 1: + # Max one argument expected + passed_args = (extracted,) + else: + passed_args = tuple(extracted) + passed_kwargs = dict(self.method_kwargs) passed_kwargs.update(kwargs) method = getattr(obj, self.method_name) diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 93e11d0..b11a4a8 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -306,13 +306,13 @@ class PostGenerationMethodCallTestCase(unittest.TestCase): def test_call_with_method_args(self): decl = declarations.PostGenerationMethodCall( - 'method', None, 'data') + 'method', 'data') decl.call(self.obj, False) self.obj.method.assert_called_once_with('data') def test_call_with_passed_extracted_string(self): decl = declarations.PostGenerationMethodCall( - 'method', None) + 'method') decl.call(self.obj, False, 'data') self.obj.method.assert_called_once_with('data') @@ -324,11 +324,11 @@ class PostGenerationMethodCallTestCase(unittest.TestCase): def test_call_with_passed_extracted_iterable(self): decl = declarations.PostGenerationMethodCall('method') decl.call(self.obj, False, (1, 2, 3)) - self.obj.method.assert_called_once_with(1, 2, 3) + self.obj.method.assert_called_once_with((1, 2, 3)) def test_call_with_method_kwargs(self): decl = declarations.PostGenerationMethodCall( - 'method', None, data='data') + 'method', data='data') decl.call(self.obj, False) self.obj.method.assert_called_once_with(data='data') @@ -337,7 +337,23 @@ class PostGenerationMethodCallTestCase(unittest.TestCase): decl.call(self.obj, False, data='other') self.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.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, ('param1', 'param2', 'param3')) + self.obj.method.assert_called_once_with('param1', 'param2', 'param3') + + def test_multi_call_with_passed_tuple(self): + decl = declarations.PostGenerationMethodCall( + 'method', 'arg1', 'arg2') + decl.call(self.obj, False, (('param1', 'param2'),)) + self.obj.method.assert_called_once_with(('param1', 'param2')) class CircularSubFactoryTestCase(unittest.TestCase): -- cgit v1.2.3 From d50993b1bda6e8c0fab1c062affa61a4e3b35216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 5 Mar 2013 22:16:54 +0100 Subject: Improve doc on post-generation hooks (Closes #36). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was the last missing bit from PR#36 by @gotgenes. Signed-off-by: Raphaël Barrois --- docs/reference.rst | 46 +++++++++++++++++++++++++++++++++++++++++++++- tests/test_declarations.py | 7 +++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 85b299c..06eee85 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -847,7 +847,7 @@ InfiniteIterator -post-building hooks +Post-generation hooks """"""""""""""""""" Some objects expect additional method calls or complex processing for proper definition. @@ -859,6 +859,49 @@ To support this pattern, factory_boy provides the following tools: - :class:`RelatedFactory`: this builds or creates a given factory *after* building/creating the first Factory. +Extracting parameters +""""""""""""""""""""" + +All post-building hooks share a common base for picking parameters from the +set of attributes passed to the :class:`Factory`. + +For instance, a :class:`PostGeneration` hook is declared as ``post``: + +.. code-block:: python + + class SomeFactory(factory.Factory): + FACTORY_FOR = SomeObject + + @post_generation + def post(self, create, extracted, **kwargs): + obj.set_origin(create) + +.. OHAI_VIM** + + +When calling the factory, some arguments will be extracted for this method: + +- If a ``post`` argument is passed, it will be passed as the ``extracted`` field +- 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. + +Thus, in the following call: + +.. code-block:: pycon + + >>> SomeFactory( + post=1, + post_x=2, + post__y=3, + post__z__t=42, + ) + +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``. + + RelatedFactory """""""""""""" @@ -1048,6 +1091,7 @@ the extracted value must be iterable: >>> UserFactory() # Calls user.set_password('', 'sha1') >>> UserFactory(password=('test', 'md5')) # Calls user.set_password('test', 'md5') + >>> UserFactory(password__disabled=True) # Calls user.set_password('', 'sha1', disabled=True) >>> # Always pass in a good iterable: >>> UserFactory(password=('test',)) # Calls user.set_password('test') diff --git a/tests/test_declarations.py b/tests/test_declarations.py index b11a4a8..2e527af 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -355,6 +355,13 @@ class PostGenerationMethodCallTestCase(unittest.TestCase): decl.call(self.obj, False, (('param1', 'param2'),)) self.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, x=2) + self.obj.method.assert_called_once_with('arg1', 'arg2', x=2) + + class CircularSubFactoryTestCase(unittest.TestCase): -- cgit v1.2.3 From d01f5e6c041395bc579f58e12e7034a08afe3f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 5 Mar 2013 22:41:49 +0100 Subject: doc: Add m2m recipes (Closes #29). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- docs/recipes.rst | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++- docs/reference.rst | 2 +- 2 files changed, 121 insertions(+), 2 deletions(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index b148cd5..e226732 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -62,6 +62,125 @@ When a :class:`UserFactory` is instantiated, factory_boy will call ``UserLogFactory(user=that_user, action=...)`` just before returning the created ``User``. +Simple ManyToMany +----------------- + +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` +or :class:`~factory.RelatedFactory`, users will have to craft their own depending +on the model. + +The base building block for this feature is the :class:`~factory.post_generation` +hook: + +.. code-block:: python + + # models.py + class Group(models.Model): + name = models.CharField() + + class User(models.Model): + name = models.CharField() + groups = models.ManyToMany(Group) + + + # factories.py + class GroupFactory(factory.DjangoModelFactory): + FACTORY_FOR = models.Group + + name = factory.Sequence(lambda n: "Group #%s" % n) + + class UserFactory(factory.DjangoModelFactory): + FACTORY_FOR = models.User + + name = "John Doe" + + @factory.post_generation + def groups(self, create, extracted, **kwargs): + if not create: + # Simple build, do nothing. + return + + if extracted: + # A list of groups were passed in, use them + for group in extracted: + self.groups.add(group) + +.. OHAI_VIM** + +When calling ``UserFactory()`` or ``UserFactory.build()``, no group binding +will be created. + +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. + + +ManyToMany 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: + +.. code-block:: python + + # models.py + class User(models.Model): + name = models.CharField() + + class Group(models.Model): + name = models.CharField() + members = models.ManyToMany(User, through='GroupLevel') + + class GroupLevel(models.Model): + user = models.ForeignKey(User) + group = models.ForeignKey(Group) + rank = models.IntegerField() + + + # factories.py + class UserFactory(factory.DjangoModelFactory): + FACTORY_FOR = models.User + + name = "John Doe" + + class GroupFactory(factory.DjangoModelFactory): + FACTORY_FOR = models.Group + + name = "Admins" + + class GroupLevelFactory(factory.DjangoModelFactory): + FACTORY_FOR = models.GroupLevel + + user = factory.SubFactory(UserFactory) + group = factory.SubFactory(GroupFactory) + rank = 1 + + class UserWithGroupFactory(UserFactory): + membership = factory.RelatedFactory(GroupLevelFactory, 'user') + + class UserWith2GroupsFactory(UserFactory): + membership1 = factory.RelatedFactory(GroupLevelFactory, 'user', group__name='Group1') + membership2 = factory.RelatedFactory(GroupLevelFactory, 'user', group__name='Group2') + + +Whenever the ``UserWithGroupFactory`` is called, it will, as a post-generation hook, +call the ``GroupLevelFactory``, passing the generated user as a ``user`` field: + +1. ``UserWithGroupFactory()`` generates a ``User`` instance, ``obj`` +2. It calls ``GroupLevelFactory(user=obj)`` +3. It returns ``obj`` + + +When using the ``UserWith2GroupsFactory``, that behavior becomes: + +1. ``UserWith2GroupsFactory()`` generates a ``User`` instance, ``obj`` +2. It calls ``GroupLevelFactory(user=obj, group__name='Group1')`` +3. It calls ``GroupLevelFactory(user=obj, group__name='Group2')`` +4. It returns ``obj`` + + Copying fields to a SubFactory ------------------------------ @@ -112,4 +231,4 @@ Here, we want: name = "ACME, Inc." country = factory.SubFactory(CountryFactory) - owner = factory.SubFactory(UserFactory, country=factory.SelfAttribute('..country)) + owner = factory.SubFactory(UserFactory, country=factory.SelfAttribute('..country')) diff --git a/docs/reference.rst b/docs/reference.rst index 06eee85..6d01e5d 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -848,7 +848,7 @@ InfiniteIterator Post-generation hooks -""""""""""""""""""" +""""""""""""""""""""" Some objects expect additional method calls or complex processing for proper definition. For instance, a ``User`` may need to have a related ``Profile``, where the ``Profile`` is built from the ``User`` object. -- cgit v1.2.3 From 114ac649e448e97a210cf8dccc6ba50278645ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 5 Mar 2013 23:12:10 +0100 Subject: Stop calling Foo.objects.create() when it doesn't break (Closes #23). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will be properly fixed in v2.0.0; the current heuristic is: - If the user defined a custom _create method, use it - If he didn't, but the associated class has a objects attribute, use TheClass.objects.create(*args, **kwargs) - Otherwise, simply call TheClass(*args, **kwargs). Signed-off-by: Raphaël Barrois --- docs/reference.rst | 9 +++++++++ factory/base.py | 3 ++- tests/test_using.py | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 6d01e5d..03023fc 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -235,6 +235,15 @@ factory_boy supports two main strategies for generating instances, plus stubs. .. OHAI_VIM* + .. warning:: For backward compatibility reasons, the default behaviour 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) diff --git a/factory/base.py b/factory/base.py index 3ebc746..44d58a7 100644 --- a/factory/base.py +++ b/factory/base.py @@ -527,7 +527,8 @@ class Factory(BaseFactory): creation_function = getattr(cls, '_creation_function', ()) if creation_function and creation_function[0]: return creation_function[0] - elif cls._create.__func__ == Factory._create.__func__: + elif cls._create.__func__ == Factory._create.__func__ and \ + hasattr(cls._associated_class, 'objects'): # Backwards compatibility. # Default creation_function and default _create() behavior. # The best "Vanilla" _create detection algorithm I found is relying diff --git a/tests/test_using.py b/tests/test_using.py index 9bc466e..8f43813 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -314,6 +314,21 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual(1, obj.id) self.assertTrue(obj.properly_created) + def test_non_django_create(self): + class NonDjango(object): + def __init__(self, x, y=2): + self.x = x + self.y = y + + class NonDjangoFactory(factory.Factory): + FACTORY_FOR = NonDjango + + x = 3 + + obj = NonDjangoFactory.create() + self.assertEqual(3, obj.x) + self.assertEqual(2, obj.y) + def test_sequence_batch(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject -- cgit v1.2.3 From c5ea364df829974aaee1d84cf70d96a125afe464 Mon Sep 17 00:00:00 2001 From: Chris Lasher Date: Thu, 7 Mar 2013 15:23:04 -0500 Subject: Merge documentation for PostGenerationMethodCall. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This merges in changes provided by gotgenes to the previous PostGeneration documentation to the new documentation provided by rbarrois. This documentation relates to the new functionality of overriding default arguments to declarations of PostGenerationMethodCall. Signed-off-by: Raphaël Barrois --- docs/reference.rst | 117 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 24 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 03023fc..70ca38a 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -863,6 +863,7 @@ Some objects expect additional method calls or complex processing for proper def For instance, a ``User`` may need to have a related ``Profile``, where the ``Profile`` is built from the ``User`` object. To support this pattern, factory_boy provides the following tools: + - :class:`PostGenerationMethodCall`: allows you to hook a particular attribute to a function call - :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. @@ -1047,60 +1048,121 @@ PostGenerationMethodCall .. class:: PostGenerationMethodCall(method_name, extract_prefix=None, *args, **kwargs) -.. OHAI_VIM* + .. OHAI_VIM* + + The :class:`PostGenerationMethodCall` declaration will call a method on + the generated object just after instantiation. This declaration class + provides a friendly means of generating attributes of a factory instance + during initialization. The declaration is created using the following arguments: + + .. attribute:: method_name + + The name of the method to call on the :attr:`~Factory.FACTORY_FOR` object -The :class:`PostGenerationMethodCall` declaration will call a method on the -generated object just after it being called. + .. attribute:: extract_prefix -Its sole argument is the name of the method to call. -Extra arguments and keyword arguments for the target method may also be provided. + If a string, the keyword argument prefix by which the field will get its + overriding arguments. If ``None``, defaults to the name of the attribute. -Once the object has been generated, the method will be called, with arguments -taken from either the :class:`PostGenerationMethodCall` or prefix-based values: + .. deprecated:: 1.3.0 + Will be removed in 2.0.0 -- If a value was extracted from kwargs (i.e an argument for the name the - :class:`PostGenerationMethodCall` was declared under): + .. attribute:: args - - If the declaration mentionned zero or one argument, the value is passed - directly to the method - - If the declaration used two or more arguments, the value is passed as - ``*args`` to the method + The default set of unnamed arguments to pass to the method given in + :attr:`method_name` -- Otherwise, the arguments used when declaring the :class:`PostGenerationMethodCall` - are used + .. attrinbute:: kwargs -- Keywords extracted from the factory arguments are merged into the defaults - present in the :class:`PostGenerationMethodCall` declaration. + The default set of keyword arguments to pass to the method given in + :attr:`method_name` + +Once the factory instance has been generated, the method specified in +:attr:`~PostGenerationMethodCall.method_name` will be called on the generated object +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: .. code-block:: python class UserFactory(factory.Factory): FACTORY_FOR = User - password = factory.PostGenerationMethodCall('set_password', password='') + username = 'user' + password = factory.PostGenerationMethodCall('set_password', + 'defaultpassword') + +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 - >>> UserFactory() # Calls user.set_password(password='') - >>> UserFactory(password='test') # Calls user.set_password('test') - >>> UserFactory(password__disabled=True) # Calls user.set_password(password='', disabled=True) + >>> u = UserFactory() # Calls user.set_password('defaultpassword') + >>> u.check_password('defaultpassword') + True + +If the :class:`PostGenerationMethodCall` declaration contained no +arguments or one argument, an overriding the 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 -When the :class:`PostGenerationMethodCall` declaration uses two or more arguments, -the extracted value must be iterable: + We can avoid this by subclassing from :class:`DjangoModelFactory`, + instead, e.g., + + .. code-block:: python + + class UserFactory(factory.DjangoModelFactory): + FACTORY_FOR = User + + username = 'user' + password = factory.PostGenerationMethodCall('set_password', + '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): FACTORY_FOR = 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') - >>> UserFactory(password__disabled=True) # Calls user.set_password('', 'sha1', disabled=True) >>> # Always pass in a good iterable: >>> UserFactory(password=('test',)) # Calls user.set_password('test') @@ -1116,6 +1178,13 @@ the extracted value must be iterable: value to the :class:`PostGenerationMethodCall` declaration (``PostGenerationMethodCall('method', 'x', 'y_that_is_the_default')``) +Keywords extracted from the factory arguments are merged into the +defaults present in the :class:`PostGenerationMethodCall` declaration. + +.. code-block:: pycon + + >>> UserFactory(password__disabled=True) # Calls user.set_password('', 'sha1', disabled=True) + Module-level functions ---------------------- -- cgit v1.2.3 From b2e798257bfb69729e8b3f6012889517ddf7b512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 17:20:36 +0100 Subject: Document need for mock/unittest. --- README | 5 +++++ tox.ini | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/README b/README index 0f20d84..a3fa2e4 100644 --- a/README +++ b/README @@ -198,6 +198,11 @@ All pull request should pass the test suite, which can be launched simply with: $ python setup.py 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 diff --git a/tox.ini b/tox.ini index 204bde9..2200f56 100644 --- a/tox.ini +++ b/tox.ini @@ -8,4 +8,10 @@ commands= [testenv:py26] deps= + mock unittest2 + +[textenv:py27] + +deps= + mock -- cgit v1.2.3 From 17097d23986b43bb74421dcbb0a0f5d75433114c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 17:21:18 +0100 Subject: Version bump to 1.3.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- docs/changelog.rst | 28 ++++++++++++++-------------- factory/__init__.py | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0671f8a..773efac 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,8 +21,8 @@ ChangeLog - Remove :meth:`~factory.Factory.set_building_function` / :meth:`~factory.Factory.set_creation_function` -1.3.0 (current) ---------------- +1.3.0 (2013-03-11) +------------------ .. warning:: This version deprecates many magic or unexplicit features that will be removed in v2.0.0. @@ -99,21 +99,21 @@ In order to upgrade client code, apply the following rules: -1.2.0 (09/08/2012) +1.2.0 (2012-09-08) ------------------ *New:* - Add :class:`~factory.CircularSubFactory` to solve circular dependencies between factories -1.1.5 (09/07/2012) +1.1.5 (2012-07-09) ------------------ *Bugfix:* - Fix :class:`~factory.PostGenerationDeclaration` and derived classes. -1.1.4 (19/06/2012) +1.1.4 (2012-06-19) ------------------ *New:* @@ -124,14 +124,14 @@ In order to upgrade client code, apply the following rules: - Introduce :class:`~factory.PostGeneration` and :class:`~factory.RelatedFactory` -1.1.3 (9/03/2012) ------------------ +1.1.3 (2012-03-09) +------------------ *Bugfix:* - Fix packaging rules -1.1.2 (25/02/2012) +1.1.2 (2012-02-25) ------------------ *New:* @@ -140,14 +140,14 @@ In order to upgrade client code, apply the following rules: - Provide :func:`~factory.Factory.generate` and :func:`~factory.Factory.simple_generate`, that allow specifying the instantiation strategy directly. Also provides :func:`~factory.Factory.generate_batch` and :func:`~factory.Factory.simple_generate_batch`. -1.1.1 (24/02/2012) +1.1.1 (2012-02-24) ------------------ *New:* - Add :func:`~factory.Factory.build_batch`, :func:`~factory.Factory.create_batch` and :func:`~factory.Factory.stub_batch`, to instantiate factories in batch -1.1.0 (24/02/2012) +1.1.0 (2012-02-24) ------------------ *New:* @@ -165,7 +165,7 @@ In order to upgrade client code, apply the following rules: - Auto-discovery of :attr:`~factory.Factory.FACTORY_FOR` based on class name is now deprecated -1.0.4 (21/12/2011) +1.0.4 (2011-12-21) ------------------ *New:* @@ -185,14 +185,14 @@ In order to upgrade client code, apply the following rules: - Share sequence counter between parent and subclasses - Fix :class:`~factory.SubFactory` / :class:`~factory.Sequence` interferences -1.0.2 (16/05/2011) +1.0.2 (2011-05-16) ------------------ *New:* - Introduce :class:`~factory.SubFactory` -1.0.1 (13/05/2011) +1.0.1 (2011-05-13) ------------------ *New:* @@ -204,7 +204,7 @@ In order to upgrade client code, apply the following rules: - Fix concurrency between :class:`~factory.LazyAttribute` and :class:`~factory.Sequence` -1.0.0 (22/08/2010) +1.0.0 (2010-08-22) ------------------ *New:* diff --git a/factory/__init__.py b/factory/__init__.py index 0cf1b82..6c56955 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__ = '1.3.0-dev' +__version__ = '1.3.0' __author__ = 'Raphaël Barrois ' from .base import ( -- cgit v1.2.3 From 91592efbdff6de4b21b3a0b1a4d86dff8818f580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:07:01 +0100 Subject: Proper manager fetching in DjangoModelFactory. --- factory/base.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/factory/base.py b/factory/base.py index 44d58a7..e798c68 100644 --- a/factory/base.py +++ b/factory/base.py @@ -659,11 +659,21 @@ class DjangoModelFactory(Factory): ABSTRACT_FACTORY = True + @classmethod + def _get_manager(cls, target_class): + try: + return target_class._default_manager + except AttributeError: + return target_class.objects + @classmethod def _setup_next_sequence(cls): """Compute the next available PK, based on the 'pk' database field.""" + + manager = cls._get_manager(cls._associated_class) + try: - return 1 + cls._associated_class._default_manager.values_list('pk', flat=True + return 1 + manager.values_list('pk', flat=True ).order_by('-pk')[0] except IndexError: return 1 @@ -671,7 +681,8 @@ class DjangoModelFactory(Factory): @classmethod def _create(cls, target_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" - return target_class._default_manager.create(*args, **kwargs) + manager = cls._get_manager(target_class) + return manager.create(*args, **kwargs) @classmethod def _after_postgeneration(cls, obj, create, results=None): -- cgit v1.2.3 From ba1fd987dad9268a1e5a41fe10513727aadfd9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:07:23 +0100 Subject: Add FACTORY_CLASS kwarg to make_factory and friends. --- docs/changelog.rst | 9 ++++----- docs/reference.rst | 44 ++++++++++++++++++++++++++++++++++---------- factory/base.py | 4 +++- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 773efac..1a82ecd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,19 +1,18 @@ ChangeLog ========= -2.0.0 (future) --------------- +2.0.0 (current) +--------------- .. note:: This section lists features planned for v2 of factory_boy. Changes announced here may not have been committed to the repository. *New:* + - Allow overriding the base factory class for :func:`~factory.make_factory` and friends. - Add support for Python3 - - Clean up documentation - - Document extension points - Add support for ``get_or_create`` in :class:`~factory.DjangoModelFactory` -*Deprecation:* +*Removed:* - Remove associated class discovery - Stop defaulting to Django's ``Foo.objects.create()`` when "creating" instances diff --git a/docs/reference.rst b/docs/reference.rst index 70ca38a..0bb0d74 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1219,6 +1219,25 @@ Lightweight factory declaration login = 'john' email = factory.LazyAttribute(lambda u: '%s@example.com' % u.login) + An alternate base class to :class:`Factory` can be specified in the + ``FACTORY_CLASS`` argument: + + .. code-block:: python + + UserFactory = make_factory(models.User, + login='john', + email=factory.LazyAttribute(lambda u: '%s@example.com' % u.login), + FACTORY_CLASS=factory.DjangoModelFactory, + ) + + # This is equivalent to: + + class UserFactory(factory.DjangoModelFactory): + FACTORY_FOR = models.User + + login = 'john' + email = factory.LazyAttribute(lambda u: '%s@example.com' % u.login) + Instance building """"""""""""""""" @@ -1226,8 +1245,8 @@ Instance building The :mod:`factory` module provides a bunch of shortcuts for creating a factory and extracting instances from them: -.. function:: build(klass, **kwargs) -.. function:: build_batch(klass, size, **kwargs) +.. 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, @@ -1236,11 +1255,12 @@ extracting instances from them: :param class 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`) -.. function:: create(klass, **kwargs) -.. function:: create_batch(klass, size, **kwargs) +.. 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; return an instance created from that factory, @@ -1249,11 +1269,12 @@ extracting instances from them: :param class 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`) -.. function:: stub(klass, **kwargs) -.. function:: stub_batch(klass, size, **kwargs) +.. 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; return an instance stubbed from that factory, @@ -1262,11 +1283,12 @@ extracting instances from them: :param class 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`) -.. function:: generate(klass, strategy, **kwargs) -.. function:: generate_batch(klass, strategy, size, **kwargs) +.. 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, @@ -1276,11 +1298,12 @@ extracting instances from them: :param str strategy: The strategy to use :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`) -.. function:: simple_generate(klass, create, **kwargs) -.. function:: simple_generate_batch(klass, create, size, **kwargs) +.. 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, @@ -1290,5 +1313,6 @@ extracting instances from them: :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 + :param FACTORY_CLASS: Alternate base class (instead of :class:`Factory`) diff --git a/factory/base.py b/factory/base.py index e798c68..a6ab98e 100644 --- a/factory/base.py +++ b/factory/base.py @@ -705,7 +705,9 @@ def make_factory(klass, **kwargs): """Create a new, simple factory for the given class.""" factory_name = '%sFactory' % klass.__name__ kwargs[FACTORY_CLASS_DECLARATION] = klass - factory_class = type(Factory).__new__(type(Factory), factory_name, (Factory,), kwargs) + base_class = kwargs.pop('FACTORY_CLASS', Factory) + + factory_class = type(Factory).__new__(type(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 -- cgit v1.2.3 From 7121fbe268b366bf543b9862ff384453edbce414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:12:43 +0100 Subject: Remove building_function/creation_function. Stop defaulting to Django's .objects.create(). --- factory/__init__.py | 4 --- factory/base.py | 92 ----------------------------------------------------- tests/test_using.py | 87 +++++++++++++++++++++++++++++++------------------- 3 files changed, 55 insertions(+), 128 deletions(-) diff --git a/factory/__init__.py b/factory/__init__.py index 6c56955..ab8005f 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -45,10 +45,6 @@ from .base import ( CREATE_STRATEGY, STUB_STRATEGY, use_strategy, - - DJANGO_CREATION, - NAIVE_BUILD, - MOGO_BUILD, ) from .declarations import ( diff --git a/factory/base.py b/factory/base.py index a6ab98e..06730ba 100644 --- a/factory/base.py +++ b/factory/base.py @@ -31,20 +31,6 @@ BUILD_STRATEGY = 'build' CREATE_STRATEGY = 'create' STUB_STRATEGY = 'stub' -# Creation functions. Deprecated. -# Override Factory._create instead. -def DJANGO_CREATION(class_to_create, **kwargs): - warnings.warn( - "Factories defaulting to Django's Foo.objects.create() is deprecated, " - "and will be removed in the future. Please inherit from " - "factory.DjangoModelFactory instead.", PendingDeprecationWarning, 6) - return class_to_create.objects.create(**kwargs) - -# Building functions. Deprecated. -# Override Factory._build instead. -NAIVE_BUILD = lambda class_to_build, **kwargs: class_to_build(**kwargs) -MOGO_BUILD = lambda class_to_build, **kwargs: class_to_build.new(**kwargs) - # Special declarations FACTORY_CLASS_DECLARATION = 'FACTORY_FOR' @@ -497,76 +483,6 @@ class Factory(BaseFactory): class AssociatedClassError(RuntimeError): pass - @classmethod - def set_creation_function(cls, creation_function): - """Set the creation function for this class. - - Args: - creation_function (function): the new creation function. That - function should take one non-keyword argument, the 'class' for - which an instance will be created. The value of the various - fields are passed as keyword arguments. - """ - warnings.warn( - "Use of factory.set_creation_function is deprecated, and will be " - "removed in the future. Please override Factory._create() instead.", - PendingDeprecationWarning, 2) - # Customizing 'create' strategy, using a tuple to keep the creation function - # from turning it into an instance method. - cls._creation_function = (creation_function,) - - @classmethod - def get_creation_function(cls): - """Retrieve the creation function for this class. - - Returns: - function: A function that takes one parameter, the class for which - an instance will be created, and keyword arguments for the value - of the fields of the instance. - """ - creation_function = getattr(cls, '_creation_function', ()) - if creation_function and creation_function[0]: - return creation_function[0] - elif cls._create.__func__ == Factory._create.__func__ and \ - hasattr(cls._associated_class, 'objects'): - # Backwards compatibility. - # Default creation_function and default _create() behavior. - # The best "Vanilla" _create detection algorithm I found is relying - # on actual method implementation (otherwise, make_factory isn't - # detected as 'default'). - return DJANGO_CREATION - - @classmethod - def set_building_function(cls, building_function): - """Set the building function for this class. - - Args: - building_function (function): the new building function. That - function should take one non-keyword argument, the 'class' for - which an instance will be built. The value of the various - fields are passed as keyword arguments. - """ - warnings.warn( - "Use of factory.set_building_function is deprecated, and will be " - "removed in the future. Please override Factory._build() instead.", - PendingDeprecationWarning, 2) - # Customizing 'build' strategy, using a tuple to keep the creation function - # from turning it into an instance method. - cls._building_function = (building_function,) - - @classmethod - def get_building_function(cls): - """Retrieve the building function for this class. - - Returns: - function: A function that takes one parameter, the class for which - an instance will be created, and keyword arguments for the value - of the fields of the instance. - """ - building_function = getattr(cls, '_building_function', ()) - if building_function and building_function[0]: - return building_function[0] - @classmethod def _adjust_kwargs(cls, **kwargs): """Extension point for custom kwargs adjustment.""" @@ -587,16 +503,8 @@ class Factory(BaseFactory): args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS) if create: - # Backwards compatibility - creation_function = cls.get_creation_function() - if creation_function: - return creation_function(target_class, *args, **kwargs) return cls._create(target_class, *args, **kwargs) else: - # Backwards compatibility - building_function = cls.get_building_function() - if building_function: - return building_function(target_class, *args, **kwargs) return cls._build(target_class, *args, **kwargs) @classmethod diff --git a/tests/test_using.py b/tests/test_using.py index 8f43813..5287a6d 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -54,6 +54,12 @@ class FakeModel(object): instance.id = 2 return instance + def values_list(self, *args, **kwargs): + return self + + def order_by(self, *args, **kwargs): + return [1] + objects = FakeModelManager() def __init__(self, **kwargs): @@ -104,19 +110,33 @@ class SimpleBuildTestCase(unittest.TestCase): self.assertEqual(obj.three, 3) self.assertEqual(obj.four, None) - @tools.disable_warnings def test_create(self): obj = factory.create(FakeModel, foo='bar') + self.assertEqual(obj.id, None) + self.assertEqual(obj.foo, 'bar') + + def test_create_custom_base(self): + obj = factory.create(FakeModel, foo='bar', FACTORY_CLASS=factory.DjangoModelFactory) self.assertEqual(obj.id, 2) self.assertEqual(obj.foo, 'bar') - @tools.disable_warnings def test_create_batch(self): objs = factory.create_batch(FakeModel, 4, foo='bar') self.assertEqual(4, len(objs)) self.assertEqual(4, len(set(objs))) + for obj in objs: + self.assertEqual(obj.id, None) + self.assertEqual(obj.foo, 'bar') + + def test_create_batch_custom_base(self): + objs = factory.create_batch(FakeModel, 4, foo='bar', + FACTORY_CLASS=factory.DjangoModelFactory) + + self.assertEqual(4, len(objs)) + self.assertEqual(4, len(set(objs))) + for obj in objs: self.assertEqual(obj.id, 2) self.assertEqual(obj.foo, 'bar') @@ -141,9 +161,14 @@ class SimpleBuildTestCase(unittest.TestCase): self.assertEqual(obj.id, None) self.assertEqual(obj.foo, 'bar') - @tools.disable_warnings def test_generate_create(self): obj = factory.generate(FakeModel, factory.CREATE_STRATEGY, foo='bar') + self.assertEqual(obj.id, None) + self.assertEqual(obj.foo, 'bar') + + def test_generate_create_custom_base(self): + obj = factory.generate(FakeModel, factory.CREATE_STRATEGY, foo='bar', + FACTORY_CLASS=factory.DjangoModelFactory) self.assertEqual(obj.id, 2) self.assertEqual(obj.foo, 'bar') @@ -162,13 +187,23 @@ class SimpleBuildTestCase(unittest.TestCase): self.assertEqual(obj.id, None) self.assertEqual(obj.foo, 'bar') - @tools.disable_warnings def test_generate_batch_create(self): objs = factory.generate_batch(FakeModel, factory.CREATE_STRATEGY, 20, foo='bar') self.assertEqual(20, len(objs)) self.assertEqual(20, len(set(objs))) + for obj in objs: + self.assertEqual(obj.id, None) + 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.DjangoModelFactory) + + self.assertEqual(20, len(objs)) + self.assertEqual(20, len(set(objs))) + for obj in objs: self.assertEqual(obj.id, 2) self.assertEqual(obj.foo, 'bar') @@ -188,9 +223,13 @@ class SimpleBuildTestCase(unittest.TestCase): self.assertEqual(obj.id, None) self.assertEqual(obj.foo, 'bar') - @tools.disable_warnings def test_simple_generate_create(self): obj = factory.simple_generate(FakeModel, True, foo='bar') + self.assertEqual(obj.id, None) + self.assertEqual(obj.foo, 'bar') + + def test_simple_generate_create_custom_base(self): + obj = factory.simple_generate(FakeModel, True, foo='bar', FACTORY_CLASS=factory.DjangoModelFactory) self.assertEqual(obj.id, 2) self.assertEqual(obj.foo, 'bar') @@ -204,13 +243,23 @@ class SimpleBuildTestCase(unittest.TestCase): self.assertEqual(obj.id, None) self.assertEqual(obj.foo, 'bar') - @tools.disable_warnings def test_simple_generate_batch_create(self): objs = factory.simple_generate_batch(FakeModel, True, 20, foo='bar') self.assertEqual(20, len(objs)) self.assertEqual(20, len(set(objs))) + for obj in objs: + self.assertEqual(obj.id, None) + 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.DjangoModelFactory) + + self.assertEqual(20, len(objs)) + self.assertEqual(20, len(set(objs))) + for obj in objs: self.assertEqual(obj.id, 2) self.assertEqual(obj.foo, 'bar') @@ -693,32 +742,6 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual('three', obj.three) self.assertEqual('four', obj.four) - @tools.disable_warnings - def test_set_building_function(self): - def building_function(class_to_create, **kwargs): - return "This doesn't even return an instance of {0}".format(class_to_create.__name__) - - class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel - - TestModelFactory.set_building_function(building_function) - - test_object = TestModelFactory.build() - self.assertEqual(test_object, "This doesn't even return an instance of TestModel") - - @tools.disable_warnings - def testSetCreationFunction(self): - def creation_function(class_to_create, **kwargs): - return "This doesn't even return an instance of {0}".format(class_to_create.__name__) - - class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel - - TestModelFactory.set_creation_function(creation_function) - - test_object = TestModelFactory.create() - self.assertEqual(test_object, "This doesn't even return an instance of TestModel") - def testClassMethodAccessible(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject -- cgit v1.2.3 From e8327fcb2e31dd7a9ffc7f53c7a678d1c1135cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:22:31 +0100 Subject: Start work on v2. --- factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/__init__.py b/factory/__init__.py index ab8005f..db88f4e 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__ = '1.3.0' +__version__ = '2.0.0-dev' __author__ = 'Raphaël Barrois ' from .base import ( -- cgit v1.2.3 From f4100b373418a58dba7ff4f29cfb44df4eca3d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:17:57 +0100 Subject: Remove automagic associated class discovery. --- factory/base.py | 36 ++++-------------------------------- tests/test_base.py | 33 +-------------------------------- 2 files changed, 5 insertions(+), 64 deletions(-) diff --git a/factory/base.py b/factory/base.py index 06730ba..8bb6d95 100644 --- a/factory/base.py +++ b/factory/base.py @@ -151,7 +151,6 @@ class FactoryMetaClass(BaseFactoryMetaClass): to a class. """ own_associated_class = None - used_auto_discovery = False if FACTORY_CLASS_DECLARATION in attrs: return attrs[FACTORY_CLASS_DECLARATION] @@ -161,37 +160,10 @@ class FactoryMetaClass(BaseFactoryMetaClass): if inherited is not None: return inherited - if '__module__' in attrs: - factory_module = sys.modules[attrs['__module__']] - if class_name.endswith('Factory'): - # Try a module lookup - used_auto_discovery = True - associated_name = class_name[:-len('Factory')] - if associated_name and hasattr(factory_module, associated_name): - warnings.warn( - "Auto-discovery of associated class is deprecated, and " - "will be removed in the future. Please set '%s = %s' " - "in the %s class definition." % ( - FACTORY_CLASS_DECLARATION, - associated_name, - class_name, - ), DeprecationWarning, 3) - - return getattr(factory_module, associated_name) - - # Unable to guess a good option; return the inherited class. - # Unable to find an associated class; fail. - if used_auto_discovery: - raise Factory.AssociatedClassError( - FactoryMetaClass.ERROR_MESSAGE_AUTODISCOVERY.format( - FACTORY_CLASS_DECLARATION, - associated_name, - class_name, - factory_module,)) - else: - raise Factory.AssociatedClassError( - FactoryMetaClass.ERROR_MESSAGE.format( - FACTORY_CLASS_DECLARATION)) + raise Factory.AssociatedClassError( + "Could not determine the class associated with %s. " + "Use the FACTORY_FOR attribute to specify an associated class." % + class_name) def __new__(cls, class_name, bases, attrs): """Determine the associated class based on the factory class name. Record the associated class diff --git a/tests/test_base.py b/tests/test_base.py index 6f16e8f..e86eae3 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -189,29 +189,6 @@ class FactoryCreationTestCase(unittest.TestCase): self.assertTrue(isinstance(TestFactory.build(), TestObject)) - def testAutomaticAssociatedClassDiscovery(self): - with warnings.catch_warnings(): - warnings.simplefilter('ignore') - class TestObjectFactory(base.Factory): - pass - - self.assertTrue(isinstance(TestObjectFactory.build(), TestObject)) - - def testDeprecationWarning(self): - """Make sure the 'auto-discovery' deprecation warning is issued.""" - - with warnings.catch_warnings(record=True) as w: - # Clear the warning registry. - __warningregistry__.clear() - - warnings.simplefilter('always') - class TestObjectFactory(base.Factory): - pass - - self.assertEqual(1, len(w)) - self.assertIn('discovery', str(w[0].message)) - self.assertIn('deprecated', str(w[0].message)) - def testStub(self): class TestFactory(base.StubFactory): pass @@ -250,15 +227,7 @@ class FactoryCreationTestCase(unittest.TestCase): # Errors - def testNoAssociatedClassWithAutodiscovery(self): - try: - class TestFactory(base.Factory): - pass - self.fail() - except base.Factory.AssociatedClassError as e: - self.assertTrue('autodiscovery' in str(e)) - - def testNoAssociatedClassWithoutAutodiscovery(self): + def test_no_associated_class(self): try: class Test(base.Factory): pass -- cgit v1.2.3 From 4e149f904cc8e0e61dd8bcc190680414fdf61767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:19:51 +0100 Subject: doc: Fix rst. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- docs/reference.rst | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 0bb0d74..e1f763c 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1072,7 +1072,7 @@ PostGenerationMethodCall The default set of unnamed arguments to pass to the method given in :attr:`method_name` - .. attrinbute:: kwargs + .. attribute:: kwargs The default set of keyword arguments to pass to the method given in :attr:`method_name` @@ -1120,21 +1120,23 @@ factory during instantiation. >>> 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()``. +.. note:: - .. code-block:: pycon + 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., + We can avoid this by subclassing from :class:`DjangoModelFactory`, + instead, e.g., - .. code-block:: python + .. code-block:: python class UserFactory(factory.DjangoModelFactory): FACTORY_FOR = User -- cgit v1.2.3 From e2ac08066fbc6b6412b713b42aba792c224067f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:20:08 +0100 Subject: Doc: Add mission 'versionadded' tag. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- docs/reference.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reference.rst b/docs/reference.rst index e1f763c..2b5ba11 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1240,6 +1240,9 @@ Lightweight factory declaration login = 'john' email = factory.LazyAttribute(lambda u: '%s@example.com' % u.login) + .. versionadded:: 2.0.0 + The ``FACTORY_CLASS`` kwarg was added in 2.0.0. + Instance building """"""""""""""""" -- cgit v1.2.3 From de3a552eab032cb980a2fb78976fc3dc8cd5f1c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:28:43 +0100 Subject: Remove InfiniteIterator and infinite_iterator. Use Iterator/iterator instead. --- docs/changelog.rst | 1 + docs/reference.rst | 26 -------------------- factory/__init__.py | 2 -- factory/declarations.py | 23 ----------------- tests/test_using.py | 65 +++---------------------------------------------- 5 files changed, 5 insertions(+), 112 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1a82ecd..88107a4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,7 @@ ChangeLog *Removed:* - Remove associated class discovery + - Remove :class:`~factory.InfiniteIterator` and :func:`~factory.infinite_iterator` - 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` diff --git a/docs/reference.rst b/docs/reference.rst index 2b5ba11..27e2e14 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -830,32 +830,6 @@ use the :func:`iterator` decorator: yield line -InfiniteIterator -~~~~~~~~~~~~~~~~ - -.. class:: InfiniteIterator(iterable) - - Equivalent to ``factory.Iterator(iterable)``. - -.. deprecated:: 1.3.0 - Merged into :class:`Iterator`; will be removed in v2.0.0. - - Replace ``factory.InfiniteIterator(iterable)`` - with ``factory.Iterator(iterable)``. - - -.. function:: infinite_iterator(function) - - Equivalent to ``factory.iterator(func)``. - - -.. deprecated:: 1.3.0 - Merged into :func:`iterator`; will be removed in v2.0.0. - - Replace ``@factory.infinite_iterator`` with ``@factory.iterator``. - - - Post-generation hooks """"""""""""""""""""" diff --git a/factory/__init__.py b/factory/__init__.py index db88f4e..4b4857c 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -50,7 +50,6 @@ from .base import ( from .declarations import ( LazyAttribute, Iterator, - InfiniteIterator, Sequence, LazyAttributeSequence, SelfAttribute, @@ -63,7 +62,6 @@ from .declarations import ( lazy_attribute, iterator, - infinite_iterator, sequence, lazy_attribute_sequence, container_attribute, diff --git a/factory/declarations.py b/factory/declarations.py index efaadbe..1f64038 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -156,21 +156,6 @@ class Iterator(OrderedDeclaration): return self.getter(value) -class InfiniteIterator(Iterator): - """Same as Iterator, but make the iterator infinite by cycling at the end. - - Attributes: - iterator (iterable): the iterator, once made infinite. - """ - - def __init__(self, iterator): - warnings.warn( - "factory.InfiniteIterator is deprecated, and will be removed in the " - "future. Please use factory.Iterator instead.", - PendingDeprecationWarning, 2) - return super(InfiniteIterator, self).__init__(iterator, cycle=True) - - class Sequence(OrderedDeclaration): """Specific OrderedDeclaration to use for 'sequenced' fields. @@ -524,14 +509,6 @@ def iterator(func): """Turn a generator function into an iterator attribute.""" return Iterator(func()) -def infinite_iterator(func): - """Turn a generator function into an infinite iterator attribute.""" - warnings.warn( - "@factory.infinite_iterator is deprecated and will be removed in the " - "future. Please use @factory.iterator instead.", - PendingDeprecationWarning, 2) - return InfiniteIterator(func()) - def sequence(func): return Sequence(func) diff --git a/tests/test_using.py b/tests/test_using.py index 5287a6d..b617668 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1137,50 +1137,24 @@ class IteratorTestCase(unittest.TestCase): for i, obj in enumerate(objs): self.assertEqual(i + 10, obj.one) - def test_infinite_iterator_deprecated(self): - with warnings.catch_warnings(record=True) as w: - __warningregistry__.clear() - - warnings.simplefilter('always') - class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject - - foo = factory.InfiniteIterator(range(5)) - - self.assertEqual(1, len(w)) - self.assertIn('InfiniteIterator', str(w[0].message)) - self.assertIn('deprecated', str(w[0].message)) - - @tools.disable_warnings - def test_infinite_iterator(self): - class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject - - one = factory.InfiniteIterator(range(5)) - - objs = TestObjectFactory.build_batch(20) - - for i, obj in enumerate(objs): - self.assertEqual(i % 5, obj.one) - @unittest.skipUnless(is_python2, "Scope bleeding fixed in Python3+") @tools.disable_warnings - def test_infinite_iterator_list_comprehension_scope_bleeding(self): + def test_iterator_list_comprehension_scope_bleeding(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject - one = factory.InfiniteIterator([j * 3 for j in range(5)]) + one = factory.Iterator([j * 3 for j in range(5)]) # Scope bleeding: j will end up in TestObjectFactory's scope. self.assertRaises(TypeError, TestObjectFactory.build) @tools.disable_warnings - def test_infinite_iterator_list_comprehension_protected(self): + def test_iterator_list_comprehension_protected(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject - one = factory.InfiniteIterator([_j * 3 for _j in range(5)]) + one = factory.Iterator([_j * 3 for _j in range(5)]) # Scope bleeding : _j will end up in TestObjectFactory's scope. # But factory_boy ignores it, as a protected variable. @@ -1203,37 +1177,6 @@ class IteratorTestCase(unittest.TestCase): for i, obj in enumerate(objs): self.assertEqual(i + 10, obj.one) - def test_infinite_iterator_decorator_deprecated(self): - with warnings.catch_warnings(record=True) as w: - __warningregistry__.clear() - - warnings.simplefilter('always') - class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject - - @factory.infinite_iterator - def one(): - return range(5) - - self.assertEqual(1, len(w)) - self.assertIn('infinite_iterator', str(w[0].message)) - self.assertIn('deprecated', str(w[0].message)) - - @tools.disable_warnings - def test_infinite_iterator_decorator(self): - class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject - - @factory.infinite_iterator - def one(): - for i in range(5): - yield i - - objs = TestObjectFactory.build_batch(20) - - for i, obj in enumerate(objs): - self.assertEqual(i % 5, obj.one) - class PostGenerationTestCase(unittest.TestCase): def test_post_generation(self): -- cgit v1.2.3 From 16e1a65f5b93615d946b74e3fb4d0b61c99ae0d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:30:45 +0100 Subject: Remove CircularSubFactory. Replace CircularSubFactory('module', 'symbol') with SubFactory('module.symbol'). --- docs/changelog.rst | 1 + docs/reference.rst | 13 ------------- factory/__init__.py | 1 - factory/declarations.py | 14 -------------- tests/test_declarations.py | 44 -------------------------------------------- 5 files changed, 1 insertion(+), 72 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 88107a4..518ab9e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,6 +16,7 @@ ChangeLog - Remove associated class discovery - Remove :class:`~factory.InfiniteIterator` and :func:`~factory.infinite_iterator` + - Remove :class:`~factory.CircularSubFactory` - 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` diff --git a/docs/reference.rst b/docs/reference.rst index 27e2e14..955d3c5 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -657,19 +657,6 @@ Obviously, such circular relationships require careful handling of loops: -.. class:: CircularSubFactory(module_name, symbol_name, **kwargs) - - .. OHAI_VIM** - - Lazily imports ``module_name.symbol_name`` at the first call. - -.. deprecated:: 1.3.0 - Merged into :class:`SubFactory`; will be removed in 2.0.0. - - Replace ``factory.CircularSubFactory('some.module', 'Symbol', **kwargs)`` - with ``factory.SubFactory('some.module.Symbol', **kwargs)`` - - SelfAttribute """"""""""""" diff --git a/factory/__init__.py b/factory/__init__.py index 4b4857c..adcf9c9 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -55,7 +55,6 @@ from .declarations import ( SelfAttribute, ContainerAttribute, SubFactory, - CircularSubFactory, PostGeneration, PostGenerationMethodCall, RelatedFactory, diff --git a/factory/declarations.py b/factory/declarations.py index 1f64038..b3c9d6a 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -325,20 +325,6 @@ class SubFactory(ParameteredAttribute): return subfactory.build(**params) -class CircularSubFactory(SubFactory): - """Use to solve circular dependencies issues.""" - def __init__(self, module_name, factory_name, **kwargs): - factory = '%s.%s' % (module_name, factory_name) - warnings.warn( - "factory.CircularSubFactory is deprecated and will be removed in " - "the future. " - "Please replace factory.CircularSubFactory('module', 'symbol') " - "with factory.SubFactory('module.symbol').", - PendingDeprecationWarning, 2) - - super(CircularSubFactory, self).__init__(factory, **kwargs) - - class PostGenerationDeclaration(object): """Declarations to be called once the target object has been generated. diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 2e527af..ceadafa 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -363,49 +363,5 @@ class PostGenerationMethodCallTestCase(unittest.TestCase): -class CircularSubFactoryTestCase(unittest.TestCase): - - def test_circularsubfactory_deprecated(self): - with warnings.catch_warnings(record=True) as w: - __warningregistry__.clear() - - warnings.simplefilter('always') - declarations.CircularSubFactory('datetime', 'date') - - self.assertEqual(1, len(w)) - self.assertIn('CircularSubFactory', str(w[0].message)) - self.assertIn('deprecated', str(w[0].message)) - - @tools.disable_warnings - def test_lazyness(self): - f = declarations.CircularSubFactory('factory.declarations', 'Sequence', x=3) - self.assertEqual(None, f.factory) - - self.assertEqual({'x': 3}, f.defaults) - - factory_class = f.get_factory() - self.assertEqual(declarations.Sequence, factory_class) - - @tools.disable_warnings - def test_cache(self): - orig_date = datetime.date - f = declarations.CircularSubFactory('datetime', 'date') - self.assertEqual(None, f.factory) - - factory_class = f.get_factory() - self.assertEqual(orig_date, factory_class) - - try: - # Modify original value - datetime.date = None - # Repeat import - factory_class = f.get_factory() - self.assertEqual(orig_date, factory_class) - - finally: - # IMPORTANT: restore attribute. - datetime.date = orig_date - - if __name__ == '__main__': unittest.main() -- cgit v1.2.3 From 60f0969406bd349a8a8b88fcaec819fa5c0525cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:36:30 +0100 Subject: Remove extract_prefix from post-generation hooks. Magic abuse is bad. --- docs/changelog.rst | 1 + docs/reference.rst | 16 ++------- factory/declarations.py | 53 ++++++----------------------- tests/test_declarations.py | 85 ---------------------------------------------- tests/test_using.py | 19 ----------- 5 files changed, 13 insertions(+), 161 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 518ab9e..b73e33b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,6 +17,7 @@ ChangeLog - Remove associated class discovery - Remove :class:`~factory.InfiniteIterator` and :func:`~factory.infinite_iterator` - Remove :class:`~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` diff --git a/docs/reference.rst b/docs/reference.rst index 955d3c5..d5b14a9 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -944,10 +944,6 @@ has been generated. Its sole argument is a callable, that will be called once the base object has been generated. -.. note:: Previous versions of factory_boy supported an extra ``extract_prefix`` - argument, to use an alternate argument prefix. - This feature is deprecated in 1.3.0 and will be removed in 2.0.0. - Once the base object has been generated, the provided callable will be called as ``callable(obj, create, extracted, **kwargs)``, where: @@ -973,7 +969,7 @@ as ``callable(obj, create, extracted, **kwargs)``, where: Decorator ~~~~~~~~~ -.. function:: post_generation(extract_prefix=None) +.. function:: post_generation A decorator is also provided, decorating a single method accepting the same ``obj``, ``created``, ``extracted`` and keyword arguments as :class:`PostGeneration`. @@ -1007,7 +1003,7 @@ A decorator is also provided, decorating a single method accepting the same PostGenerationMethodCall """""""""""""""""""""""" -.. class:: PostGenerationMethodCall(method_name, extract_prefix=None, *args, **kwargs) +.. class:: PostGenerationMethodCall(method_name, *args, **kwargs) .. OHAI_VIM* @@ -1020,14 +1016,6 @@ PostGenerationMethodCall The name of the method to call on the :attr:`~Factory.FACTORY_FOR` object - .. attribute:: extract_prefix - - If a string, the keyword argument prefix by which the field will get its - overriding arguments. If ``None``, defaults to the name of the attribute. - - .. deprecated:: 1.3.0 - Will be removed in 2.0.0 - .. attribute:: args The default set of unnamed arguments to pass to the method given in diff --git a/factory/declarations.py b/factory/declarations.py index b3c9d6a..b491bfb 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -326,21 +326,7 @@ class SubFactory(ParameteredAttribute): class PostGenerationDeclaration(object): - """Declarations to be called once the target object has been generated. - - Attributes: - extract_prefix (str): prefix to use when extracting attributes from - the factory's declaration for this declaration. If empty, uses - the attribute name of the PostGenerationDeclaration. - """ - - def __init__(self, extract_prefix=None): - if extract_prefix: - warnings.warn( - "The extract_prefix argument to PostGeneration declarations " - "is deprecated and will be removed in the future.", - PendingDeprecationWarning, 3) - self.extract_prefix = extract_prefix + """Declarations to be called once the target object has been generated.""" def extract(self, name, attrs): """Extract relevant attributes from a dict. @@ -355,12 +341,8 @@ class PostGenerationDeclaration(object): (object, dict): a tuple containing the attribute at 'name' (if provided) and a dict of extracted attributes """ - if self.extract_prefix: - extract_prefix = self.extract_prefix - else: - extract_prefix = name - extracted = attrs.pop(extract_prefix, None) - kwargs = utils.extract_dict(extract_prefix, attrs) + extracted = attrs.pop(name, None) + kwargs = utils.extract_dict(name, attrs) return extracted, kwargs def call(self, obj, create, extracted=None, **kwargs): # pragma: no cover @@ -369,7 +351,7 @@ class PostGenerationDeclaration(object): Args: obj (object): the newly generated object create (bool): whether the object was 'built' or 'created' - extracted (object): the value given for in the + extracted (object): the value given for in the object definition, or None if not provided. kwargs (dict): declarations extracted from the object definition for this hook @@ -379,30 +361,16 @@ class PostGenerationDeclaration(object): class PostGeneration(PostGenerationDeclaration): """Calls a given function once the object has been generated.""" - def __init__(self, function, extract_prefix=None): - super(PostGeneration, self).__init__(extract_prefix) + def __init__(self, function): + super(PostGeneration, self).__init__() self.function = function def call(self, obj, create, extracted=None, **kwargs): return self.function(obj, create, extracted, **kwargs) -def post_generation(*args, **kwargs): - assert len(args) + len(kwargs) <= 1, "post_generation takes at most one argument." - if args and callable(args[0]): - # Called as @post_generation applied to a function - return PostGeneration(args[0]) - else: - warnings.warn( - "The @post_generation should now be applied directly to the " - "function, without parameters. The @post_generation() and " - "@post_generation(extract_prefix='xxx') syntaxes are deprecated " - "and will be removed in the future; use @post_generation instead.", - PendingDeprecationWarning, 2) - extract_prefix = kwargs.get('extract_prefix') - def decorator(fun): - return PostGeneration(fun, extract_prefix=extract_prefix) - return decorator +def post_generation(fun): + return PostGeneration(fun) class RelatedFactory(PostGenerationDeclaration): @@ -416,7 +384,7 @@ class RelatedFactory(PostGenerationDeclaration): """ def __init__(self, factory, name='', **defaults): - super(RelatedFactory, self).__init__(extract_prefix=None) + super(RelatedFactory, self).__init__() self.name = name self.defaults = defaults @@ -464,8 +432,7 @@ class PostGenerationMethodCall(PostGenerationDeclaration): password = factory.PostGenerationMethodCall('set_password', password='') """ def __init__(self, method_name, *args, **kwargs): - extract_prefix = kwargs.pop('extract_prefix', None) - super(PostGenerationMethodCall, self).__init__(extract_prefix) + super(PostGenerationMethodCall, self).__init__() self.method_name = method_name self.method_args = args self.method_kwargs = kwargs diff --git a/tests/test_declarations.py b/tests/test_declarations.py index ceadafa..b7ae344 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -122,26 +122,6 @@ class PostGenerationDeclarationTestCase(unittest.TestCase): self.assertEqual(extracted, 13) self.assertEqual(kwargs, {'bar': 42}) - @tools.disable_warnings - def test_extract_with_prefix(self): - decl = declarations.PostGenerationDeclaration(extract_prefix='blah') - - extracted, kwargs = decl.extract('foo', - {'foo': 13, 'foo__bar': 42, 'blah': 42, 'blah__baz': 1}) - self.assertEqual(extracted, 42) - self.assertEqual(kwargs, {'baz': 1}) - - def test_extract_prefix_deprecated(self): - with warnings.catch_warnings(record=True) as w: - __warningregistry__.clear() - - warnings.simplefilter('always') - declarations.PostGenerationDeclaration(extract_prefix='blah') - - self.assertEqual(1, len(w)) - self.assertIn('extract_prefix', str(w[0].message)) - self.assertIn('deprecated', str(w[0].message)) - def test_decorator_simple(self): call_params = [] @declarations.post_generation @@ -160,71 +140,6 @@ class PostGenerationDeclarationTestCase(unittest.TestCase): self.assertEqual((None, False, 13), call_params[0]) self.assertEqual({'bar': 42}, call_params[1]) - @tools.disable_warnings - def test_decorator_call_no_prefix(self): - call_params = [] - @declarations.post_generation() - def foo(*args, **kwargs): - call_params.append(args) - call_params.append(kwargs) - - extracted, kwargs = foo.extract('foo', - {'foo': 13, 'foo__bar': 42, 'blah': 42, 'blah__baz': 1}) - self.assertEqual(13, extracted) - self.assertEqual({'bar': 42}, kwargs) - - # No value returned. - foo.call(None, False, extracted, **kwargs) - self.assertEqual(2, len(call_params)) - self.assertEqual((None, False, 13), call_params[0]) - self.assertEqual({'bar': 42}, call_params[1]) - - @tools.disable_warnings - def test_decorator_extract_prefix(self): - call_params = [] - @declarations.post_generation(extract_prefix='blah') - def foo(*args, **kwargs): - call_params.append(args) - call_params.append(kwargs) - - extracted, kwargs = foo.extract('foo', - {'foo': 13, 'foo__bar': 42, 'blah': 42, 'blah__baz': 1}) - self.assertEqual(42, extracted) - self.assertEqual({'baz': 1}, kwargs) - - # No value returned. - foo.call(None, False, extracted, **kwargs) - self.assertEqual(2, len(call_params)) - self.assertEqual((None, False, 42), call_params[0]) - self.assertEqual({'baz': 1}, call_params[1]) - - def test_decorator_call_no_prefix_deprecated(self): - with warnings.catch_warnings(record=True) as w: - __warningregistry__.clear() - - warnings.simplefilter('always') - @declarations.post_generation() - def foo(*args, **kwargs): - pass - - self.assertEqual(1, len(w)) - self.assertIn('post_generation', str(w[0].message)) - self.assertIn('deprecated', str(w[0].message)) - - def test_decorator_call_with_prefix_deprecated(self): - with warnings.catch_warnings(record=True) as w: - __warningregistry__.clear() - - warnings.simplefilter('always') - @declarations.post_generation(extract_prefix='blah') - def foo(*args, **kwargs): - pass - - # 2 warnings: decorator with brackets, and extract_prefix. - self.assertEqual(2, len(w)) - self.assertIn('post_generation', str(w[0].message)) - self.assertIn('deprecated', str(w[0].message)) - class SubFactoryTestCase(unittest.TestCase): diff --git a/tests/test_using.py b/tests/test_using.py index b617668..a0b0db7 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1218,25 +1218,6 @@ class PostGenerationTestCase(unittest.TestCase): self.assertFalse(obj.create) self.assertEqual({'incr_one': 42}, obj.results) - @tools.disable_warnings - def test_post_generation_calling(self): - class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject - - one = 1 - - @factory.post_generation() - def incr_one(self, _create, _increment): - self.one += 1 - - obj = TestObjectFactory.build() - self.assertEqual(2, obj.one) - self.assertFalse(hasattr(obj, 'incr_one')) - - obj = TestObjectFactory.build(one=2) - self.assertEqual(3, obj.one) - self.assertFalse(hasattr(obj, 'incr_one')) - def test_post_generation_extraction(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject -- cgit v1.2.3 From d63821daba2002b8c455777748007f7198d3d3bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 15 Mar 2013 01:08:00 +0100 Subject: Remove unused constants. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/factory/base.py b/factory/base.py index 8bb6d95..25d7a14 100644 --- a/factory/base.py +++ b/factory/base.py @@ -120,12 +120,6 @@ class BaseFactoryMetaClass(type): class FactoryMetaClass(BaseFactoryMetaClass): """Factory metaclass for handling class association and ordered declarations.""" - ERROR_MESSAGE = """Could not determine what class this factory is for. - Use the {0} attribute to specify a class.""" - ERROR_MESSAGE_AUTODISCOVERY = ERROR_MESSAGE + """ - Also, autodiscovery failed using the name '{1}' - based on the Factory name '{2}' in {3}.""" - @classmethod def _discover_associated_class(cls, class_name, attrs, inherited=None): """Try to find the class associated with this factory. -- cgit v1.2.3 From 6e9bf5af909e1e164a294fd5589edc4fada06731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 15 Mar 2013 01:33:56 +0100 Subject: Merge BaseFactoryMetaClass into FactoryMetaClass. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also fix FACTORY_STRATEGY. Signed-off-by: Raphaël Barrois --- docs/introduction.rst | 2 +- factory/base.py | 200 ++++++++++++++++++++++++++------------------------ tests/test_base.py | 22 +++--- 3 files changed, 117 insertions(+), 107 deletions(-) diff --git a/docs/introduction.rst b/docs/introduction.rst index d211a83..8bbb10c 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -253,6 +253,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.default_strategy` attribute. +The default strategy can ba changed by setting the class-level :attr:`~factory.Factory.FACTROY_STRATEGY` attribute. diff --git a/factory/base.py b/factory/base.py index 25d7a14..aef21d5 100644 --- a/factory/base.py +++ b/factory/base.py @@ -44,11 +44,11 @@ CLASS_ATTRIBUTE_ASSOCIATED_CLASS = '_associated_class' # Factory metaclasses def get_factory_bases(bases): - """Retrieve all BaseFactoryMetaClass-derived bases from a list.""" - return [b for b in bases if isinstance(b, BaseFactoryMetaClass)] + """Retrieve all FactoryMetaClass-derived bases from a list.""" + return [b for b in bases if issubclass(b, BaseFactory)] -class BaseFactoryMetaClass(type): +class FactoryMetaClass(type): """Factory metaclass for handling ordered declarations.""" def __call__(cls, **kwargs): @@ -57,68 +57,14 @@ class BaseFactoryMetaClass(type): Returns an instance of the associated class. """ - if cls.default_strategy == BUILD_STRATEGY: + if cls.FACTORY_STRATEGY == BUILD_STRATEGY: return cls.build(**kwargs) - elif cls.default_strategy == CREATE_STRATEGY: + elif cls.FACTORY_STRATEGY == CREATE_STRATEGY: return cls.create(**kwargs) - elif cls.default_strategy == STUB_STRATEGY: + elif cls.FACTORY_STRATEGY == STUB_STRATEGY: return cls.stub(**kwargs) else: - raise BaseFactory.UnknownStrategy('Unknown default_strategy: {0}'.format(cls.default_strategy)) - - def __new__(cls, class_name, bases, attrs, extra_attrs=None): - """Record attributes as a pattern for later instance construction. - - 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 - extra_attrs (str => obj dict): extra attributes that should not be - included in the factory defaults, even if public. This - argument is only provided by extensions of this metaclass. - - Returns: - A new class - """ - - parent_factories = get_factory_bases(bases) - if not parent_factories: - # If this isn't a subclass of Factory, don't do anything special. - return super(BaseFactoryMetaClass, cls).__new__(cls, class_name, bases, attrs) - - declarations = containers.DeclarationDict() - postgen_declarations = containers.PostGenerationDeclarationDict() - - # Add parent declarations in reverse order. - for base in reversed(parent_factories): - # 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, {})) - - # Import attributes from the class definition - non_postgen_attrs = postgen_declarations.update_with_public(attrs) - # Store protected/private attributes in 'non_factory_attrs'. - non_factory_attrs = declarations.update_with_public(non_postgen_attrs) - - # Store the DeclarationDict in the attributes of the newly created class - non_factory_attrs[CLASS_ATTRIBUTE_DECLARATIONS] = declarations - non_factory_attrs[CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS] = postgen_declarations - - # Add extra args if provided. - if extra_attrs: - non_factory_attrs.update(extra_attrs) - - return super(BaseFactoryMetaClass, cls).__new__(cls, class_name, bases, non_factory_attrs) - - -class FactoryMetaClass(BaseFactoryMetaClass): - """Factory metaclass for handling class association and ordered declarations.""" + raise BaseFactory.UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format(cls.FACTORY_STRATEGY)) @classmethod def _discover_associated_class(cls, class_name, attrs, inherited=None): @@ -126,8 +72,6 @@ class FactoryMetaClass(BaseFactoryMetaClass): In order, the following tests will be performed: - Lookup the FACTORY_CLASS_DECLARATION attribute - - If the newly created class is named 'FooBarFactory', look for a FooBar - class in its module - If an inherited associated class was provided, use it. Args: @@ -154,46 +98,103 @@ class FactoryMetaClass(BaseFactoryMetaClass): if inherited is not None: return inherited - raise Factory.AssociatedClassError( + raise AssociatedClassError( "Could not determine the class associated with %s. " "Use the FACTORY_FOR attribute to specify an associated class." % class_name) - def __new__(cls, class_name, bases, attrs): - """Determine the associated class based on the factory class name. Record the associated class - for construction of an associated class instance at a later time.""" + @classmethod + def _extract_declarations(cls, bases, attributes): + """Extract declarations from a class definition. - parent_factories = get_factory_bases(bases) - if not parent_factories or attrs.get('ABSTRACT_FACTORY', False): - # If this isn't a subclass of Factory, or specifically declared - # abstract, don't do anything special. - if 'ABSTRACT_FACTORY' in attrs: - attrs.pop('ABSTRACT_FACTORY') + Args: + bases (class list): parent Factory subclasses + attributes (dict): attributes declared in the class definition + 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() + + # 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, {})) + + # 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) + + # Store the DeclarationDict in the attributes of the newly created class + attributes[CLASS_ATTRIBUTE_DECLARATIONS] = declarations + attributes[CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS] = postgen_declarations + + return attributes + + def __new__(cls, class_name, bases, attrs, extra_attrs=None): + """Record attributes as a pattern for later instance construction. + + 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 + extra_attrs (str => obj dict): extra attributes that should not be + included in the factory defaults, even if public. This + argument is only provided by extensions of this metaclass. + + Returns: + A new class + """ + parent_factories = get_factory_bases(bases) + if not parent_factories: return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs) - base = parent_factories[0] + is_abstract = attrs.pop('ABSTRACT_FACTORY', False) + extra_attrs = {} + + if not is_abstract: + + base = parent_factories[0] - inherited_associated_class = getattr(base, - CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None) - associated_class = cls._discover_associated_class(class_name, attrs, - inherited_associated_class) + inherited_associated_class = getattr(base, + CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None) + associated_class = cls._discover_associated_class(class_name, attrs, + inherited_associated_class) - # If inheriting the factory from a parent, keep a link to it. - # This allows to use the sequence counters from the parents. - if associated_class == inherited_associated_class: - attrs['_base_factory'] = base + # If inheriting the factory from a parent, keep a link to it. + # This allows to use the sequence counters from the parents. + if associated_class == inherited_associated_class: + attrs['_base_factory'] = base - # 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} + # 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} - return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs, extra_attrs=extra_attrs) + # Extract pre- and post-generation declarations + attributes = cls._extract_declarations(parent_factories, attrs) + + # Add extra args if provided. + if extra_attrs: + attributes.update(extra_attrs) + + return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attributes) def __str__(self): return '<%s for %s>' % (self.__name__, getattr(self, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__) + # Factory base classes class BaseFactory(object): @@ -430,10 +431,8 @@ class BaseFactory(object): return cls.generate_batch(strategy, size, **kwargs) -class StubFactory(BaseFactory): - __metaclass__ = BaseFactoryMetaClass - - default_strategy = STUB_STRATEGY +class AssociatedClassError(RuntimeError): + pass class Factory(BaseFactory): @@ -444,10 +443,8 @@ class Factory(BaseFactory): """ __metaclass__ = FactoryMetaClass - default_strategy = CREATE_STRATEGY - - class AssociatedClassError(RuntimeError): - pass + ABSTRACT_FACTORY = True + FACTORY_STRATEGY = CREATE_STRATEGY @classmethod def _adjust_kwargs(cls, **kwargs): @@ -522,6 +519,19 @@ class Factory(BaseFactory): return cls._generate(True, attrs) +Factory.AssociatedClassError = AssociatedClassError + + +class StubFactory(BaseFactory): + __metaclass__ = FactoryMetaClass + + FACTORY_STRATEGY = STUB_STRATEGY + + FACTORY_FOR = containers.StubObject + +print StubFactory._associated_class + + class DjangoModelFactory(Factory): """Factory for Django models. @@ -643,6 +653,6 @@ def use_strategy(new_strategy): This is an alternative to setting default_strategy in the class definition. """ def wrapped_class(klass): - klass.default_strategy = new_strategy + klass.FACTORY_STRATEGY = new_strategy return klass return wrapped_class diff --git a/tests/test_base.py b/tests/test_base.py index e86eae3..e12c0ae 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -108,13 +108,13 @@ class FactoryTestCase(unittest.TestCase): class FactoryDefaultStrategyTestCase(unittest.TestCase): def setUp(self): - self.default_strategy = base.Factory.default_strategy + self.default_strategy = base.Factory.FACTORY_STRATEGY def tearDown(self): - base.Factory.default_strategy = self.default_strategy + base.Factory.FACTORY_STRATEGY = self.default_strategy def testBuildStrategy(self): - base.Factory.default_strategy = base.BUILD_STRATEGY + base.Factory.FACTORY_STRATEGY = base.BUILD_STRATEGY class TestModelFactory(base.Factory): FACTORY_FOR = TestModel @@ -126,7 +126,7 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): self.assertFalse(test_model.id) def testCreateStrategy(self): - # Default default_strategy + # Default FACTORY_STRATEGY class TestModelFactory(FakeModelFactory): FACTORY_FOR = TestModel @@ -138,7 +138,7 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): self.assertTrue(test_model.id) def testStubStrategy(self): - base.Factory.default_strategy = base.STUB_STRATEGY + base.Factory.FACTORY_STRATEGY = base.STUB_STRATEGY class TestModelFactory(base.Factory): FACTORY_FOR = TestModel @@ -150,7 +150,7 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): self.assertFalse(hasattr(test_model, 'id')) # We should have a plain old object def testUnknownStrategy(self): - base.Factory.default_strategy = 'unknown' + base.Factory.FACTORY_STRATEGY = 'unknown' class TestModelFactory(base.Factory): FACTORY_FOR = TestModel @@ -165,11 +165,11 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): one = 'one' - TestModelFactory.default_strategy = base.CREATE_STRATEGY + TestModelFactory.FACTORY_STRATEGY = base.CREATE_STRATEGY self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory) - TestModelFactory.default_strategy = base.BUILD_STRATEGY + TestModelFactory.FACTORY_STRATEGY = base.BUILD_STRATEGY self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory) def test_change_strategy(self): @@ -179,7 +179,7 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): one = 'one' - self.assertEqual(base.CREATE_STRATEGY, TestModelFactory.default_strategy) + self.assertEqual(base.CREATE_STRATEGY, TestModelFactory.FACTORY_STRATEGY) class FactoryCreationTestCase(unittest.TestCase): @@ -193,7 +193,7 @@ class FactoryCreationTestCase(unittest.TestCase): class TestFactory(base.StubFactory): pass - self.assertEqual(TestFactory.default_strategy, base.STUB_STRATEGY) + self.assertEqual(TestFactory.FACTORY_STRATEGY, base.STUB_STRATEGY) def testInheritanceWithStub(self): class TestObjectFactory(base.StubFactory): @@ -204,7 +204,7 @@ class FactoryCreationTestCase(unittest.TestCase): class TestFactory(TestObjectFactory): pass - self.assertEqual(TestFactory.default_strategy, base.STUB_STRATEGY) + self.assertEqual(TestFactory.FACTORY_STRATEGY, base.STUB_STRATEGY) def testCustomCreation(self): class TestModelFactory(FakeModelFactory): -- cgit v1.2.3 From efc0ca41dcec074176064faf1e899ea275bb2901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 15 Mar 2013 01:39:57 +0100 Subject: Fix exception hierarchy. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 31 ++++++++++++++++++++----------- tests/test_base.py | 2 +- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/factory/base.py b/factory/base.py index aef21d5..2418675 100644 --- a/factory/base.py +++ b/factory/base.py @@ -41,6 +41,22 @@ CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS = '_postgen_declarations' CLASS_ATTRIBUTE_ASSOCIATED_CLASS = '_associated_class' +class FactoryError(Exception): + """Any exception raised by factory_boy.""" + + +class AssociatedClassError(FactoryError): + """Exception for Factory subclasses lacking FACTORY_FOR.""" + + +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): @@ -64,7 +80,7 @@ class FactoryMetaClass(type): elif cls.FACTORY_STRATEGY == STUB_STRATEGY: return cls.stub(**kwargs) else: - raise BaseFactory.UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format(cls.FACTORY_STRATEGY)) + raise UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format(cls.FACTORY_STRATEGY)) @classmethod def _discover_associated_class(cls, class_name, attrs, inherited=None): @@ -200,15 +216,12 @@ class FactoryMetaClass(type): class BaseFactory(object): """Factory base support for sequences, attributes and stubs.""" - class UnknownStrategy(RuntimeError): - pass - - class UnsupportedStrategy(RuntimeError): - pass + UnknownStrategy = UnknownStrategy + UnsupportedStrategy = UnsupportedStrategy def __new__(cls, *args, **kwargs): """Would be called if trying to instantiate the class.""" - raise RuntimeError('You cannot instantiate BaseFactory') + raise FactoryError('You cannot instantiate BaseFactory') # ID to use for the next 'declarations.Sequence' attribute. _next_sequence = None @@ -431,10 +444,6 @@ class BaseFactory(object): return cls.generate_batch(strategy, size, **kwargs) -class AssociatedClassError(RuntimeError): - pass - - class Factory(BaseFactory): """Factory base with build and create support. diff --git a/tests/test_base.py b/tests/test_base.py index e12c0ae..fb3ba30 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -60,7 +60,7 @@ class TestModel(FakeDjangoModel): class SafetyTestCase(unittest.TestCase): def testBaseFactory(self): - self.assertRaises(RuntimeError, base.BaseFactory) + self.assertRaises(base.FactoryError, base.BaseFactory) class FactoryTestCase(unittest.TestCase): -- cgit v1.2.3 From 66e4f2eadeae535e10f2318d4d4ed7041337ce5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 15 Mar 2013 01:43:07 +0100 Subject: Merge Factory into BaseFactory. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 148 +++++++++++++++++++++++++++----------------------------- 1 file changed, 72 insertions(+), 76 deletions(-) diff --git a/factory/base.py b/factory/base.py index 2418675..17bd267 100644 --- a/factory/base.py +++ b/factory/base.py @@ -289,6 +289,68 @@ class BaseFactory(object): """ return getattr(cls, CLASS_ATTRIBUTE_DECLARATIONS).copy(extra_defs) + @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 + """ + target_class = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS) + kwargs = cls._adjust_kwargs(**kwargs) + + # Extract *args from **kwargs + args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS) + + if create: + return cls._create(target_class, *args, **kwargs) + else: + return cls._build(target_class, *args, **kwargs) + + @classmethod + def _generate(cls, create, attrs): + """generate the object. + + Args: + create (bool): whether to 'build' or 'create' the object + attrs (dict): attributes to use for generating the object + """ + # Extract declarations used for post-generation + postgen_declarations = getattr(cls, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS) + postgen_attributes = {} + for name, decl in sorted(postgen_declarations.items()): + postgen_attributes[name] = decl.extract(name, attrs) + + # Generate the object + obj = cls._prepare(create, **attrs) + + # Handle post-generation attributes + results = {} + for name, decl in sorted(postgen_declarations.items()): + extracted, extracted_kwargs = postgen_attributes[name] + results[name] = decl.call(obj, create, extracted, **extracted_kwargs) + + cls._after_postgeneration(obj, create, results) + + return obj + + @classmethod + def _after_postgeneration(cls, obj, create, results=None): + """Hook called after post-generation declarations have been handled. + + Args: + obj (object): the generated object + create (bool): whether the strategy was 'build' or 'create' + results (dict or None): result of post-generation declarations + """ + pass + @classmethod def _build(cls, target_class, *args, **kwargs): """Actually build an instance of the target_class. @@ -322,7 +384,8 @@ class BaseFactory(object): @classmethod def build(cls, **kwargs): """Build an instance of the associated class, with overriden attrs.""" - raise cls.UnsupportedStrategy() + attrs = cls.attributes(create=False, extra=kwargs) + return cls._generate(False, attrs) @classmethod def build_batch(cls, size, **kwargs): @@ -339,7 +402,8 @@ class BaseFactory(object): @classmethod def create(cls, **kwargs): """Create an instance of the associated class, with overriden attrs.""" - raise cls.UnsupportedStrategy() + attrs = cls.attributes(create=True, extra=kwargs) + return cls._generate(True, attrs) @classmethod def create_batch(cls, size, **kwargs): @@ -455,90 +519,22 @@ class Factory(BaseFactory): ABSTRACT_FACTORY = True FACTORY_STRATEGY = CREATE_STRATEGY - @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 - """ - target_class = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS) - kwargs = cls._adjust_kwargs(**kwargs) - - # Extract *args from **kwargs - args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS) - - if create: - return cls._create(target_class, *args, **kwargs) - else: - return cls._build(target_class, *args, **kwargs) - - @classmethod - def _generate(cls, create, attrs): - """generate the object. - - Args: - create (bool): whether to 'build' or 'create' the object - attrs (dict): attributes to use for generating the object - """ - # Extract declarations used for post-generation - postgen_declarations = getattr(cls, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS) - postgen_attributes = {} - for name, decl in sorted(postgen_declarations.items()): - postgen_attributes[name] = decl.extract(name, attrs) - - # Generate the object - obj = cls._prepare(create, **attrs) - - # Handle post-generation attributes - results = {} - for name, decl in sorted(postgen_declarations.items()): - extracted, extracted_kwargs = postgen_attributes[name] - results[name] = decl.call(obj, create, extracted, **extracted_kwargs) - - cls._after_postgeneration(obj, create, results) +Factory.AssociatedClassError = AssociatedClassError - return obj - @classmethod - def _after_postgeneration(cls, obj, create, results=None): - """Hook called after post-generation declarations have been handled. +class StubFactory(Factory): - Args: - obj (object): the generated object - create (bool): whether the strategy was 'build' or 'create' - results (dict or None): result of post-generation declarations - """ - pass + FACTORY_STRATEGY = STUB_STRATEGY + FACTORY_FOR = containers.StubObject @classmethod def build(cls, **kwargs): - attrs = cls.attributes(create=False, extra=kwargs) - return cls._generate(False, attrs) + raise UnsupportedStrategy() @classmethod def create(cls, **kwargs): - attrs = cls.attributes(create=True, extra=kwargs) - return cls._generate(True, attrs) - - -Factory.AssociatedClassError = AssociatedClassError - - -class StubFactory(BaseFactory): - __metaclass__ = FactoryMetaClass - - FACTORY_STRATEGY = STUB_STRATEGY - - FACTORY_FOR = containers.StubObject - -print StubFactory._associated_class + raise UnsupportedStrategy() class DjangoModelFactory(Factory): -- cgit v1.2.3 From 624aedf03974bedb34349d0664fb863935e99969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 15 Mar 2013 01:45:27 +0100 Subject: Make the Factory class Py3 compatible. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/factory/base.py b/factory/base.py index 17bd267..f3d5eab 100644 --- a/factory/base.py +++ b/factory/base.py @@ -508,18 +508,18 @@ class BaseFactory(object): return cls.generate_batch(strategy, size, **kwargs) -class Factory(BaseFactory): - """Factory base with build and create support. +Factory = FactoryMetaClass('Factory', (BaseFactory,), { + 'ABSTRACT_FACTORY': True, + 'FACTORY_STRATEGY': CREATE_STRATEGY, + '__doc__': """Factory base with build and create support. This class has the ability to support multiple ORMs by using custom creation functions. - """ - __metaclass__ = FactoryMetaClass - - ABSTRACT_FACTORY = True - FACTORY_STRATEGY = CREATE_STRATEGY + """, + }) +# Backwards compatibility Factory.AssociatedClassError = AssociatedClassError -- cgit v1.2.3 From 6c4f5846c8e21d6e48347b7e661edb72ffabb9f1 Mon Sep 17 00:00:00 2001 From: nkryptic Date: Tue, 12 Mar 2013 01:08:59 -0400 Subject: Add full Python 3 compatibility (Closes #10, #20, #49). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also: - update travis.yml to build against 2.6-2.7 and 3.2-3.3 - Switch to relative imports Signed-off-by: Raphaël Barrois --- .travis.yml | 12 ++++++++++-- README | 2 +- docs/changelog.rst | 2 +- factory/base.py | 2 +- factory/compat.py | 33 +++++++++++++++++++++++++++++++++ factory/containers.py | 4 ++-- factory/declarations.py | 7 ++++--- factory/utils.py | 3 ++- setup.py | 3 +++ tests/compat.py | 2 +- 10 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 factory/compat.py diff --git a/.travis.yml b/.travis.yml index aa990e6..e32214b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,17 @@ language: python + python: - "2.6" - "2.7" -script: "python setup.py test" -install: "if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi" + - "3.2" + - "3.3" + +script: + - python setup.py test + +install: + - "if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi" + notifications: email: false irc: "irc.freenode.org#factory_boy" diff --git a/README b/README index a3fa2e4..a4629af 100644 --- a/README +++ b/README @@ -22,7 +22,7 @@ Links * Official repository: https://github.com/rbarrois/factory_boy * Package: https://pypi.python.org/pypi/factory_boy/ -factory_boy supports Python 2.6 and 2.7 (Python 3 is in the works), and requires only the standard Python library. +factory_boy supports Python 2.6, 2.7, 3.2 and 3.3; and requires only the standard Python library. Download diff --git a/docs/changelog.rst b/docs/changelog.rst index b73e33b..695670f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,7 +9,7 @@ ChangeLog *New:* - Allow overriding the base factory class for :func:`~factory.make_factory` and friends. - - Add support for Python3 + - Add support for Python3 (Thanks to `kmike `_ and `nkryptic `_) - Add support for ``get_or_create`` in :class:`~factory.DjangoModelFactory` *Removed:* diff --git a/factory/base.py b/factory/base.py index f3d5eab..ff3e558 100644 --- a/factory/base.py +++ b/factory/base.py @@ -24,7 +24,7 @@ import re import sys import warnings -from factory import containers +from . import containers # Strategies BUILD_STRATEGY = 'build' diff --git a/factory/compat.py b/factory/compat.py new file mode 100644 index 0000000..a924de0 --- /dev/null +++ b/factory/compat.py @@ -0,0 +1,33 @@ +# -*- 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. + + +"""Compatibility tools""" + +import sys + +is_python2 = (sys.version_info[0] == 2) + +if is_python2: + string_types = (str, unicode) +else: + string_types = (str,) diff --git a/factory/containers.py b/factory/containers.py index 31ee58b..0859a10 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -21,8 +21,8 @@ # THE SOFTWARE. -from factory import declarations -from factory import utils +from . import declarations +from . import utils class CyclicDefinitionError(Exception): diff --git a/factory/declarations.py b/factory/declarations.py index b491bfb..2b1fc05 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -25,7 +25,8 @@ import collections import itertools import warnings -from factory import utils +from . import compat +from . import utils class OrderedDeclaration(object): @@ -294,7 +295,7 @@ class SubFactory(ParameteredAttribute): self.factory_module = self.factory_name = '' else: # Must be a string - if not isinstance(factory, basestring) or '.' not in factory: + if not isinstance(factory, compat.string_types) or '.' not in factory: raise ValueError( "The argument of a SubFactory must be either a class " "or the fully qualified path to a Factory class; got " @@ -393,7 +394,7 @@ class RelatedFactory(PostGenerationDeclaration): self.factory_module = self.factory_name = '' else: # Must be a string - if not isinstance(factory, basestring) or '.' not in factory: + if not isinstance(factory, compat.string_types) or '.' not in factory: raise ValueError( "The argument of a SubFactory must be either a class " "or the fully qualified path to a Factory class; got " diff --git a/factory/utils.py b/factory/utils.py index 90fdfc3..fb8cfef 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -43,7 +43,8 @@ def extract_dict(prefix, kwargs, pop=True, exclude=()): """ prefix = prefix + ATTR_SPLITTER extracted = {} - for key in kwargs.keys(): + + for key in list(kwargs): if key in exclude: continue diff --git a/setup.py b/setup.py index ef3da96..1cb2e91 100755 --- a/setup.py +++ b/setup.py @@ -87,6 +87,9 @@ setup( 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', 'Topic :: Software Development :: Testing', 'Topic :: Software Development :: Libraries :: Python Modules' ], diff --git a/tests/compat.py b/tests/compat.py index 769ffd4..6a1eb80 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -30,7 +30,7 @@ try: except ImportError: import unittest -if is_python2: +if sys.version_info[0:2] < (3, 3): import mock else: from unittest import mock -- cgit v1.2.3 From 4d6847eb93aa269130ad803fb50be99c4a7508d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 24 Mar 2013 20:02:08 +0100 Subject: Default Sequence.type to int (Closes #50). --- docs/changelog.rst | 1 + factory/declarations.py | 2 +- tests/test_containers.py | 8 ++++---- tests/test_using.py | 30 +++++++++++++++--------------- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 695670f..b9ea79f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,7 @@ ChangeLog - Allow overriding the base factory class for :func:`~factory.make_factory` and friends. - Add support for Python3 (Thanks to `kmike `_ and `nkryptic `_) - Add support for ``get_or_create`` in :class:`~factory.DjangoModelFactory` + - The default :attr:`~factory.Sequence.type` for :class:`~factory.Sequence` is now :obj:`int` *Removed:* diff --git a/factory/declarations.py b/factory/declarations.py index 2b1fc05..a284930 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -168,7 +168,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=str): + def __init__(self, function, type=int): super(Sequence, self).__init__() self.function = function self.type = type diff --git a/tests/test_containers.py b/tests/test_containers.py index 4f70e29..70ed885 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -253,7 +253,7 @@ class AttributeBuilderTestCase(unittest.TestCase): self.assertEqual({'one': 2}, ab.build(create=False)) def test_factory_defined_sequence(self): - seq = declarations.Sequence(lambda n: 'xx' + n) + seq = declarations.Sequence(lambda n: 'xx%d' % n) class FakeFactory(object): @classmethod @@ -270,7 +270,7 @@ class AttributeBuilderTestCase(unittest.TestCase): self.assertEqual({'one': 'xx1'}, ab.build(create=False)) def test_additionnal_sequence(self): - seq = declarations.Sequence(lambda n: 'xx' + n) + seq = declarations.Sequence(lambda n: 'xx%d' % n) class FakeFactory(object): @classmethod @@ -287,8 +287,8 @@ class AttributeBuilderTestCase(unittest.TestCase): self.assertEqual({'one': 1, 'two': 'xx1'}, ab.build(create=False)) def test_replaced_sequence(self): - seq = declarations.Sequence(lambda n: 'xx' + n) - seq2 = declarations.Sequence(lambda n: 'yy' + n) + seq = declarations.Sequence(lambda n: 'xx%d' % n) + seq2 = declarations.Sequence(lambda n: 'yy%d' % n) class FakeFactory(object): @classmethod diff --git a/tests/test_using.py b/tests/test_using.py index a0b0db7..2e07621 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -315,8 +315,8 @@ class UsingFactoryTestCase(unittest.TestCase): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject - one = factory.Sequence(lambda n: 'one' + n) - two = factory.Sequence(lambda n: 'two' + n) + one = factory.Sequence(lambda n: 'one%d' % n) + two = factory.Sequence(lambda n: 'two%d' % n) test_object0 = TestObjectFactory.build() self.assertEqual(test_object0.one, 'one0') @@ -334,8 +334,8 @@ class UsingFactoryTestCase(unittest.TestCase): def _setup_next_sequence(cls): return 42 - one = factory.Sequence(lambda n: 'one' + n) - two = factory.Sequence(lambda n: 'two' + n) + one = factory.Sequence(lambda n: 'one%d' % n) + two = factory.Sequence(lambda n: 'two%d' % n) test_object0 = TestObjectFactory.build() self.assertEqual('one42', test_object0.one) @@ -382,8 +382,8 @@ class UsingFactoryTestCase(unittest.TestCase): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject - one = factory.Sequence(lambda n: 'one' + n) - two = factory.Sequence(lambda n: 'two' + n) + one = factory.Sequence(lambda n: 'one%d' % n) + two = factory.Sequence(lambda n: 'two%d' % n) objs = TestObjectFactory.build_batch(20) @@ -408,8 +408,8 @@ class UsingFactoryTestCase(unittest.TestCase): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject - one = factory.LazyAttributeSequence(lambda a, n: 'abc' + n) - two = factory.LazyAttributeSequence(lambda a, n: a.one + ' xyz' + n) + one = factory.LazyAttributeSequence(lambda a, n: 'abc%d' % n) + two = factory.LazyAttributeSequence(lambda a, n: a.one + ' xyz%d' % n) test_object0 = TestObjectFactory.build() self.assertEqual(test_object0.one, 'abc0') @@ -472,7 +472,7 @@ class UsingFactoryTestCase(unittest.TestCase): @factory.sequence def one(n): - return 'one' + n + return 'one%d' % n test_object = TestObjectFactory.build() self.assertEqual(test_object.one, 'one0') @@ -483,10 +483,10 @@ class UsingFactoryTestCase(unittest.TestCase): @factory.lazy_attribute_sequence def one(a, n): - return 'one' + n + return 'one%d' % n @factory.lazy_attribute_sequence def two(a, n): - return a.one + ' two' + n + return a.one + ' two%d' % n test_object = TestObjectFactory.build() self.assertEqual(test_object.one, 'one0') @@ -496,8 +496,8 @@ class UsingFactoryTestCase(unittest.TestCase): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject - one = factory.Sequence(lambda n: 'one' + n) - two = factory.Sequence(lambda n: 'two' + n) + one = factory.Sequence(lambda n: 'one%d' % n) + two = factory.Sequence(lambda n: 'two%d' % n) test_object0 = TestObjectFactory.build(three='three') self.assertEqual(test_object0.one, 'one0') @@ -673,7 +673,7 @@ class UsingFactoryTestCase(unittest.TestCase): three = factory.Sequence(lambda n: int(n)) objs = TestObjectFactory.stub_batch(20, - one=factory.Sequence(lambda n: n)) + one=factory.Sequence(lambda n: str(n))) self.assertEqual(20, len(objs)) self.assertEqual(20, len(set(objs))) @@ -862,7 +862,7 @@ class SubFactoryTestCase(unittest.TestCase): class TestModel2Factory(FakeModelFactory): FACTORY_FOR = TestModel2 two = factory.SubFactory(TestModelFactory, - one=factory.Sequence(lambda n: 'x%sx' % n), + one=factory.Sequence(lambda n: 'x%dx' % n), two=factory.LazyAttribute( lambda o: '%s%s' % (o.one, o.one))) -- cgit v1.2.3 From 7a7bfc750605701cb7d82656e918a5b375fbc385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 28 Mar 2013 23:42:32 +0100 Subject: Advertise PyPy support. --- .travis.yml | 1 + README | 2 +- setup.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e32214b..d0735d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - "2.7" - "3.2" - "3.3" + - "pypy" script: - python setup.py test diff --git a/README b/README index a4629af..3756109 100644 --- a/README +++ b/README @@ -22,7 +22,7 @@ Links * Official 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; and requires only the standard Python library. +factory_boy supports Python 2.6, 2.7, 3.2 and 3.3, as well as PyPy; it requires only the standard Python library. Download diff --git a/setup.py b/setup.py index 1cb2e91..050e43b 100755 --- a/setup.py +++ b/setup.py @@ -90,6 +90,7 @@ setup( 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Testing', 'Topic :: Software Development :: Libraries :: Python Modules' ], -- cgit v1.2.3 From 54a915fa25a66d3b7732a096eba7c2dd4a7b5a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 2 Apr 2013 22:51:15 +0200 Subject: declarations: minor code simplification --- factory/declarations.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index a284930..2122bd2 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -320,10 +320,7 @@ class SubFactory(ParameteredAttribute): override the wrapped factory's defaults """ subfactory = self.get_factory() - if create: - return subfactory.create(**params) - else: - return subfactory.build(**params) + return subfactory.simple_generate(create, **params) class PostGenerationDeclaration(object): -- cgit v1.2.3 From 69e7a86875f97dc12e941302fabe417122f2cb7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 2 Apr 2013 23:07:03 +0200 Subject: Add Factory.FACTORY_HIDDEN_ARGS. Fields listed in this class attributes will be removed from the kwargs dict passed to the associated class for building/creation. --- docs/changelog.rst | 5 ++++- docs/ideas.rst | 3 --- docs/reference.rst | 31 ++++++++++++++++++++++++++++ factory/base.py | 7 +++++++ tests/test_using.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b9ea79f..9a4a64e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,8 +10,11 @@ ChangeLog - Allow overriding the base factory class for :func:`~factory.make_factory` and friends. - Add support for Python3 (Thanks to `kmike `_ and `nkryptic `_) - - Add support for ``get_or_create`` in :class:`~factory.DjangoModelFactory` - The default :attr:`~factory.Sequence.type` for :class:`~factory.Sequence` is now :obj:`int` + - Fields listed in :attr:`~factory.Factory.FACTORY_HIDDEN_ARGS` won't be passed to + the associated class' constructor + + - Add support for ``get_or_create`` in :class:`~factory.DjangoModelFactory` *Removed:* diff --git a/docs/ideas.rst b/docs/ideas.rst index 004f722..914e640 100644 --- a/docs/ideas.rst +++ b/docs/ideas.rst @@ -5,7 +5,4 @@ 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 -* **factory-local fields**: Allow some fields to be available while building attributes, - but not passed to the associated class. - For instance, a ``global_kind`` field would be used to select values for many other fields. diff --git a/docs/reference.rst b/docs/reference.rst index d5b14a9..a2af327 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -50,6 +50,37 @@ The :class:`Factory` class >>> User('john', 'john@example.com', firstname="John") # actual call + .. attribute:: FACTORY_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 + 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',) + + now = factory.LazyAttribute(lambda o: 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)) + + .. code-block:: pycon + + >>> OrderFactory() # The value of 'now' isn't passed to Order() + + + >>> # An alternate value may be passed for 'now' + >>> OrderFactory(now=datetime.datetime(2013, 4, 1, 10)) + + + **Base functions:** The :class:`Factory` class provides a few methods for getting objects; diff --git a/factory/base.py b/factory/base.py index ff3e558..58cd50b 100644 --- a/factory/base.py +++ b/factory/base.py @@ -234,6 +234,9 @@ class BaseFactory(object): # 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 = () + @classmethod def _setup_next_sequence(cls): """Set up an initial sequence value for Sequence attributes. @@ -305,6 +308,10 @@ class BaseFactory(object): target_class = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS) kwargs = cls._adjust_kwargs(**kwargs) + # Remove 'hidden' arguments. + for arg in cls.FACTORY_HIDDEN_ARGS: + del kwargs[arg] + # Extract *args from **kwargs args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS) diff --git a/tests/test_using.py b/tests/test_using.py index 2e07621..41b666f 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -762,6 +762,65 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual(TestObjectFactory.alt_create(foo=1), {"foo": 1}) + def test_arg_parameters(self): + class TestObject(object): + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + FACTORY_ARG_PARAMETERS = ('x', 'y') + + x = 1 + y = 2 + z = 3 + t = 4 + + obj = TestObjectFactory.build(x=42, z=5) + self.assertEqual((42, 2), obj.args) + self.assertEqual({'z': 5, 't': 4}, obj.kwargs) + + def test_hidden_args(self): + class TestObject(object): + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + FACTORY_HIDDEN_ARGS = ('x', 'z') + + x = 1 + y = 2 + z = 3 + t = 4 + + obj = TestObjectFactory.build(x=42, z=5) + self.assertEqual((), obj.args) + self.assertEqual({'y': 2, 't': 4}, obj.kwargs) + + def test_hidden_args_and_arg_parameters(self): + class TestObject(object): + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + FACTORY_HIDDEN_ARGS = ('x', 'z') + FACTORY_ARG_PARAMETERS = ('y',) + + x = 1 + y = 2 + z = 3 + t = 4 + + obj = TestObjectFactory.build(x=42, z=5) + self.assertEqual((2,), obj.args) + self.assertEqual({'t': 4}, obj.kwargs) + + class NonKwargParametersTestCase(unittest.TestCase): def test_build(self): -- cgit v1.2.3 From 6532f25058a13e81b1365bb353848510821f571f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 2 Apr 2013 23:34:28 +0200 Subject: Add support for get_or_create in DjangoModelFactory. --- docs/changelog.rst | 4 +- docs/orms.rst | 33 +++++++++++++ factory/base.py | 15 +++++- tests/test_using.py | 133 +++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 180 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9a4a64e..4b24797 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,8 +13,8 @@ ChangeLog - The default :attr:`~factory.Sequence.type` for :class:`~factory.Sequence` is now :obj:`int` - Fields listed in :attr:`~factory.Factory.FACTORY_HIDDEN_ARGS` won't be passed to the associated class' constructor - - - Add support for ``get_or_create`` in :class:`~factory.DjangoModelFactory` + - Add support for ``get_or_create`` in :class:`~factory.DjangoModelFactory`, + through :attr:`~factory.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE`. *Removed:* diff --git a/docs/orms.rst b/docs/orms.rst index d6ff3c3..8e5b6f6 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -35,3 +35,36 @@ All factories for a Django :class:`~django.db.models.Model` should use the * When using :class:`~factory.RelatedFactory` or :class:`~factory.PostGeneration` attributes, the base object will be :meth:`saved ` once all post-generation hooks have run. + + .. attribute:: FACTORY_DJANGO_GET_OR_CREATE + + 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() `: + + .. code-block:: python + + class UserFactory(factory.DjangoModelFactory): + FACTORY_FOR = models.User + FACTORY_DJANGO_GET_OR_CREATE = ('username',) + + username = 'john' + + .. code-block:: pycon + + >>> User.objects.all() + [] + >>> UserFactory() # Creates a new user + + >>> User.objects.all() + [] + + >>> UserFactory() # Fetches the existing user + + >>> User.objects.all() # No new user! + [] + + >>> UserFactory(username='jack') # Creates another user + + >>> User.objects.all() + [, ] diff --git a/factory/base.py b/factory/base.py index 58cd50b..71c2eb1 100644 --- a/factory/base.py +++ b/factory/base.py @@ -554,6 +554,7 @@ class DjangoModelFactory(Factory): """ ABSTRACT_FACTORY = True + FACTORY_DJANGO_GET_OR_CREATE = () @classmethod def _get_manager(cls, target_class): @@ -578,7 +579,19 @@ class DjangoModelFactory(Factory): 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) - return manager.create(*args, **kwargs) + + assert 'defaults' not in cls.FACTORY_DJANGO_GET_OR_CREATE, ( + "'defaults' is a reserved keyword for get_or_create " + "(in %s.FACTORY_DJANGO_GET_OR_CREATE=%r)" + % (cls, cls.FACTORY_DJANGO_GET_OR_CREATE)) + + key_fields = {} + for field in cls.FACTORY_DJANGO_GET_OR_CREATE: + key_fields[field] = kwargs.pop(field) + key_fields['defaults'] = kwargs + + obj, created = manager.get_or_create(*args, **key_fields) + return obj @classmethod def _after_postgeneration(cls, obj, create, results=None): diff --git a/tests/test_using.py b/tests/test_using.py index 41b666f..65cb7a5 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -49,10 +49,11 @@ class FakeModel(object): return instance class FakeModelManager(object): - def create(self, **kwargs): + def get_or_create(self, **kwargs): + kwargs.update(kwargs.pop('defaults', {})) instance = FakeModel.create(**kwargs) instance.id = 2 - return instance + return instance, True def values_list(self, *args, **kwargs): return self @@ -1237,6 +1238,134 @@ class IteratorTestCase(unittest.TestCase): self.assertEqual(i + 10, obj.one) +class BetterFakeModelManager(object): + def __init__(self, keys, instance): + self.keys = keys + self.instance = instance + + def get_or_create(self, **kwargs): + defaults = kwargs.pop('defaults', {}) + if kwargs == self.keys: + return self.instance, False + kwargs.update(defaults) + instance = FakeModel.create(**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 + def create(cls, **kwargs): + instance = cls(**kwargs) + instance.id = 1 + return instance + + def __init__(self, **kwargs): + for name, value in kwargs.items(): + setattr(self, name, value) + self.id = None + + +class DjangoModelFactoryTestCase(unittest.TestCase): + def test_simple(self): + class FakeModelFactory(factory.DjangoModelFactory): + FACTORY_FOR = FakeModel + + obj = FakeModelFactory(one=1) + self.assertEqual(1, obj.one) + self.assertEqual(2, obj.id) + + def test_existing_instance(self): + prev = BetterFakeModel.create(x=1, y=2, z=3) + prev.id = 42 + + class MyFakeModel(BetterFakeModel): + objects = BetterFakeModelManager({'x': 1}, prev) + + class MyFakeModelFactory(factory.DjangoModelFactory): + FACTORY_FOR = MyFakeModel + FACTORY_DJANGO_GET_OR_CREATE = ('x',) + x = 1 + y = 4 + z = 6 + + obj = MyFakeModelFactory() + self.assertEqual(prev, obj) + self.assertEqual(1, obj.x) + self.assertEqual(2, obj.y) + self.assertEqual(3, obj.z) + self.assertEqual(42, obj.id) + + def test_existing_instance_complex_key(self): + prev = BetterFakeModel.create(x=1, y=2, z=3) + prev.id = 42 + + class MyFakeModel(BetterFakeModel): + objects = BetterFakeModelManager({'x': 1, 'y': 2, 'z': 3}, prev) + + class MyFakeModelFactory(factory.DjangoModelFactory): + FACTORY_FOR = MyFakeModel + FACTORY_DJANGO_GET_OR_CREATE = ('x', 'y', 'z') + x = 1 + y = 4 + z = 6 + + obj = MyFakeModelFactory(y=2, z=3) + self.assertEqual(prev, obj) + self.assertEqual(1, obj.x) + self.assertEqual(2, obj.y) + self.assertEqual(3, obj.z) + self.assertEqual(42, obj.id) + + def test_new_instance(self): + prev = BetterFakeModel.create(x=1, y=2, z=3) + prev.id = 42 + + class MyFakeModel(BetterFakeModel): + objects = BetterFakeModelManager({'x': 1}, prev) + + class MyFakeModelFactory(factory.DjangoModelFactory): + FACTORY_FOR = MyFakeModel + FACTORY_DJANGO_GET_OR_CREATE = ('x',) + x = 1 + y = 4 + z = 6 + + obj = MyFakeModelFactory(x=2) + self.assertNotEqual(prev, obj) + self.assertEqual(2, obj.x) + self.assertEqual(4, obj.y) + self.assertEqual(6, obj.z) + self.assertEqual(2, obj.id) + + def test_new_instance_complex_key(self): + prev = BetterFakeModel.create(x=1, y=2, z=3) + prev.id = 42 + + class MyFakeModel(BetterFakeModel): + objects = BetterFakeModelManager({'x': 1, 'y': 2, 'z': 3}, prev) + + class MyFakeModelFactory(factory.DjangoModelFactory): + FACTORY_FOR = MyFakeModel + FACTORY_DJANGO_GET_OR_CREATE = ('x', 'y', 'z') + x = 1 + y = 4 + z = 6 + + obj = MyFakeModelFactory(y=2, z=4) + self.assertNotEqual(prev, obj) + self.assertEqual(1, obj.x) + self.assertEqual(2, obj.y) + self.assertEqual(4, obj.z) + self.assertEqual(2, obj.id) + + class PostGenerationTestCase(unittest.TestCase): def test_post_generation(self): class TestObjectFactory(factory.Factory): -- cgit v1.2.3 From 4c0d0650610e154499511d27268ed7c1d32b60db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Apr 2013 00:42:05 +0200 Subject: internal: merge OrderedDeclaration.evaluate() variants. --- factory/containers.py | 51 +++++++++++++++++----------------------------- factory/declarations.py | 39 +++++++++++++++++++++++++++-------- tests/test_declarations.py | 24 +++++++++++----------- 3 files changed, 62 insertions(+), 52 deletions(-) diff --git a/factory/containers.py b/factory/containers.py index 0859a10..dc3a457 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -172,30 +172,6 @@ class LazyValue(object): raise NotImplementedError("This is an abstract method.") -class SubFactoryWrapper(LazyValue): - """Lazy wrapper around a SubFactory. - - Attributes: - subfactory (declarations.SubFactory): the SubFactory being wrapped - subfields (DeclarationDict): Default values to override when evaluating - the SubFactory - create (bool): whether to 'create' or 'build' the SubFactory. - """ - - def __init__(self, subfactory, subfields, create, *args, **kwargs): - super(SubFactoryWrapper, self).__init__(*args, **kwargs) - self.subfactory = subfactory - self.subfields = subfields - self.create = create - - def evaluate(self, obj, containers=()): - expanded_containers = (obj,) - if containers: - expanded_containers += tuple(containers) - return self.subfactory.evaluate(self.create, self.subfields, - expanded_containers) - - class OrderedDeclarationWrapper(LazyValue): """Lazy wrapper around an OrderedDeclaration. @@ -206,10 +182,12 @@ class OrderedDeclarationWrapper(LazyValue): declaration """ - def __init__(self, declaration, sequence, *args, **kwargs): - super(OrderedDeclarationWrapper, self).__init__(*args, **kwargs) + def __init__(self, declaration, sequence, create, extra=None, **kwargs): + super(OrderedDeclarationWrapper, 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. @@ -219,7 +197,14 @@ class OrderedDeclarationWrapper(LazyValue): containers (object list): the chain of containers of the object being built, its immediate holder being first. """ - return self.declaration.evaluate(self.sequence, obj, containers) + 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): @@ -240,7 +225,7 @@ class AttributeBuilder(object): extra = {} self.factory = factory - self._containers = extra.pop('__containers', None) + self._containers = extra.pop('__containers', ()) self._attrs = factory.declarations(extra) attrs_with_subfields = [k for k, v in self._attrs.items() if self.has_subfields(v)] @@ -263,10 +248,12 @@ class AttributeBuilder(object): # OrderedDeclaration. wrapped_attrs = {} for k, v in self._attrs.items(): - if isinstance(v, declarations.SubFactory): - v = SubFactoryWrapper(v, self._subfields.get(k, {}), create) - elif isinstance(v, declarations.OrderedDeclaration): - v = OrderedDeclarationWrapper(v, self.factory.sequence) + if isinstance(v, declarations.OrderedDeclaration): + v = OrderedDeclarationWrapper(v, + sequence=self.factory.sequence, + create=create, + extra=self._subfields.get(k, {}), + ) wrapped_attrs[k] = v stub = LazyStub(wrapped_attrs, containers=self._containers, diff --git a/factory/declarations.py b/factory/declarations.py index 2122bd2..15d8d5b 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -37,7 +37,7 @@ class OrderedDeclaration(object): in the same factory. """ - def evaluate(self, sequence, obj, containers=()): + def evaluate(self, sequence, obj, create, extra=None, containers=()): """Evaluate this declaration. Args: @@ -47,6 +47,10 @@ class OrderedDeclaration(object): 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 + 'created' + extra (DeclarationDict or None): extracted key/value extracted from + the attribute prefix """ raise NotImplementedError('This is an abstract method') @@ -63,7 +67,7 @@ class LazyAttribute(OrderedDeclaration): super(LazyAttribute, self).__init__(*args, **kwargs) self.function = function - def evaluate(self, sequence, obj, containers=()): + def evaluate(self, sequence, obj, create, extra=None, containers=()): return self.function(obj) @@ -122,7 +126,7 @@ class SelfAttribute(OrderedDeclaration): self.attribute_name = attribute_name self.default = default - def evaluate(self, sequence, obj, containers=()): + def evaluate(self, sequence, obj, create, extra=None, containers=()): if self.depth > 1: # Fetching from a parent target = containers[self.depth - 2] @@ -130,6 +134,13 @@ class SelfAttribute(OrderedDeclaration): target = obj return deepgetattr(target, self.attribute_name, self.default) + def __repr__(self): + return '<%s(%r, default=%r)>' % ( + self.__class__.__name__, + self.attribute_name, + self.default, + ) + class Iterator(OrderedDeclaration): """Fill this value using the values returned by an iterator. @@ -150,7 +161,7 @@ class Iterator(OrderedDeclaration): else: self.iterator = iter(iterator) - def evaluate(self, sequence, obj, containers=()): + def evaluate(self, sequence, obj, create, extra=None, containers=()): value = next(self.iterator) if self.getter is None: return value @@ -173,7 +184,7 @@ class Sequence(OrderedDeclaration): self.function = function self.type = type - def evaluate(self, sequence, obj, containers=()): + def evaluate(self, sequence, obj, create, extra=None, containers=()): return self.function(self.type(sequence)) @@ -186,7 +197,7 @@ 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, containers=()): + def evaluate(self, sequence, obj, create, extra=None, containers=()): return self.function(obj, self.type(sequence)) @@ -204,7 +215,7 @@ class ContainerAttribute(OrderedDeclaration): self.function = function self.strict = strict - def evaluate(self, sequence, obj, containers=()): + def evaluate(self, sequence, obj, create, extra=None, containers=()): """Evaluate the current ContainerAttribute. Args: @@ -237,11 +248,20 @@ class ParameteredAttribute(OrderedDeclaration): CONTAINERS_FIELD = '__containers' + # Whether to add the current object to the stack of containers + EXTEND_CONTAINERS = False + def __init__(self, **kwargs): super(ParameteredAttribute, self).__init__() self.defaults = kwargs - def evaluate(self, create, extra, containers): + def _prepare_containers(self, obj, containers=()): + if self.EXTEND_CONTAINERS: + return (obj,) + tuple(containers) + + return containers + + def evaluate(self, sequence, obj, create, extra=None, containers=()): """Evaluate the current definition and fill its attributes. Uses attributes definition in the following order: @@ -260,6 +280,7 @@ class ParameteredAttribute(OrderedDeclaration): if extra: defaults.update(extra) if self.CONTAINERS_FIELD: + containers = self._prepare_containers(obj, containers) defaults[self.CONTAINERS_FIELD] = containers return self.generate(create, defaults) @@ -288,6 +309,8 @@ class SubFactory(ParameteredAttribute): factory (base.Factory): the wrapped factory """ + EXTEND_CONTAINERS = True + def __init__(self, factory, **kwargs): super(SubFactory, self).__init__(**kwargs) if isinstance(factory, type): diff --git a/tests/test_declarations.py b/tests/test_declarations.py index b7ae344..7b9b0af 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -33,7 +33,7 @@ from . import tools class OrderedDeclarationTestCase(unittest.TestCase): def test_errors(self): decl = declarations.OrderedDeclaration() - self.assertRaises(NotImplementedError, decl.evaluate, None, {}) + self.assertRaises(NotImplementedError, decl.evaluate, None, {}, False) class DigTestCase(unittest.TestCase): @@ -95,23 +95,23 @@ class SelfAttributeTestCase(unittest.TestCase): class IteratorTestCase(unittest.TestCase): def test_cycle(self): it = declarations.Iterator([1, 2]) - self.assertEqual(1, it.evaluate(0, None)) - self.assertEqual(2, it.evaluate(1, None)) - self.assertEqual(1, it.evaluate(2, None)) - self.assertEqual(2, it.evaluate(3, None)) + 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)) def test_no_cycling(self): it = declarations.Iterator([1, 2], cycle=False) - self.assertEqual(1, it.evaluate(0, None)) - self.assertEqual(2, it.evaluate(1, None)) - self.assertRaises(StopIteration, it.evaluate, 2, None) + self.assertEqual(1, it.evaluate(0, None, False)) + self.assertEqual(2, it.evaluate(1, None, False)) + self.assertRaises(StopIteration, it.evaluate, 2, None, False) def test_getter(self): it = declarations.Iterator([(1, 2), (1, 3)], getter=lambda p: p[1]) - self.assertEqual(2, it.evaluate(0, None)) - self.assertEqual(3, it.evaluate(1, None)) - self.assertEqual(2, it.evaluate(2, None)) - self.assertEqual(3, it.evaluate(3, None)) + 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)) class PostGenerationDeclarationTestCase(unittest.TestCase): -- cgit v1.2.3 From 3aee208ee7cdf480cbc173cf3084ce2217a5944f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Apr 2013 00:46:38 +0200 Subject: Nit: cleanup name of test methods. --- tests/test_base.py | 26 +++++++++++++------------- tests/test_using.py | 50 +++++++++++++++++++++++++------------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index fb3ba30..969ef13 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -59,7 +59,7 @@ class TestModel(FakeDjangoModel): class SafetyTestCase(unittest.TestCase): - def testBaseFactory(self): + def test_base_factory(self): self.assertRaises(base.FactoryError, base.BaseFactory) @@ -72,14 +72,14 @@ class FactoryTestCase(unittest.TestCase): obj = TestObjectFactory.build() self.assertFalse(hasattr(obj, 'FACTORY_FOR')) - def testDisplay(self): + def test_display(self): class TestObjectFactory(base.Factory): FACTORY_FOR = FakeDjangoModel self.assertIn('TestObjectFactory', str(TestObjectFactory)) self.assertIn('FakeDjangoModel', str(TestObjectFactory)) - def testLazyAttributeNonExistentParam(self): + def test_lazy_attribute_non_existent_param(self): class TestObjectFactory(base.Factory): FACTORY_FOR = TestObject @@ -87,7 +87,7 @@ class FactoryTestCase(unittest.TestCase): self.assertRaises(AttributeError, TestObjectFactory) - def testInheritanceWithSequence(self): + def test_inheritance_with_sequence(self): """Tests that sequence IDs are shared between parent and son.""" class TestObjectFactory(base.Factory): FACTORY_FOR = TestObject @@ -113,7 +113,7 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): def tearDown(self): base.Factory.FACTORY_STRATEGY = self.default_strategy - def testBuildStrategy(self): + def test_build_strategy(self): base.Factory.FACTORY_STRATEGY = base.BUILD_STRATEGY class TestModelFactory(base.Factory): @@ -125,7 +125,7 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): self.assertEqual(test_model.one, 'one') self.assertFalse(test_model.id) - def testCreateStrategy(self): + def test_create_strategy(self): # Default FACTORY_STRATEGY class TestModelFactory(FakeModelFactory): @@ -137,7 +137,7 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): self.assertEqual(test_model.one, 'one') self.assertTrue(test_model.id) - def testStubStrategy(self): + def test_stub_strategy(self): base.Factory.FACTORY_STRATEGY = base.STUB_STRATEGY class TestModelFactory(base.Factory): @@ -149,7 +149,7 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): self.assertEqual(test_model.one, 'one') self.assertFalse(hasattr(test_model, 'id')) # We should have a plain old object - def testUnknownStrategy(self): + def test_unknown_strategy(self): base.Factory.FACTORY_STRATEGY = 'unknown' class TestModelFactory(base.Factory): @@ -159,7 +159,7 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): self.assertRaises(base.Factory.UnknownStrategy, TestModelFactory) - def testStubWithNonStubStrategy(self): + def test_stub_with_non_stub_strategy(self): class TestModelFactory(base.StubFactory): FACTORY_FOR = TestModel @@ -183,19 +183,19 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): class FactoryCreationTestCase(unittest.TestCase): - def testFactoryFor(self): + def test_factory_for(self): class TestFactory(base.Factory): FACTORY_FOR = TestObject self.assertTrue(isinstance(TestFactory.build(), TestObject)) - def testStub(self): + def test_stub(self): class TestFactory(base.StubFactory): pass self.assertEqual(TestFactory.FACTORY_STRATEGY, base.STUB_STRATEGY) - def testInheritanceWithStub(self): + def test_inheritance_with_stub(self): class TestObjectFactory(base.StubFactory): FACTORY_FOR = TestObject @@ -206,7 +206,7 @@ class FactoryCreationTestCase(unittest.TestCase): self.assertEqual(TestFactory.FACTORY_STRATEGY, base.STUB_STRATEGY) - def testCustomCreation(self): + def test_custom_creation(self): class TestModelFactory(FakeModelFactory): FACTORY_FOR = TestModel diff --git a/tests/test_using.py b/tests/test_using.py index 65cb7a5..dde0ba7 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -282,7 +282,7 @@ class SimpleBuildTestCase(unittest.TestCase): class UsingFactoryTestCase(unittest.TestCase): - def testAttribute(self): + def test_attribute(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -312,7 +312,7 @@ class UsingFactoryTestCase(unittest.TestCase): test_object = InheritedFactory.build() self.assertEqual(test_object.one, 'one') - def testSequence(self): + def test_sequence(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -327,7 +327,7 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual(test_object1.one, 'one1') self.assertEqual(test_object1.two, 'two1') - def testSequenceCustomBegin(self): + def test_sequence_custom_begin(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -394,7 +394,7 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual('one%d' % i, obj.one) self.assertEqual('two%d' % i, obj.two) - def testLazyAttribute(self): + def test_lazy_attribute(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -405,7 +405,7 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual(test_object.one, 'abc') self.assertEqual(test_object.two, 'abc xyz') - def testLazyAttributeSequence(self): + def test_lazy_attribute_sequence(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -420,7 +420,7 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual(test_object1.one, 'abc1') self.assertEqual(test_object1.two, 'abc1 xyz1') - def testLazyAttributeDecorator(self): + def test_lazy_attribute_decorator(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -431,7 +431,7 @@ class UsingFactoryTestCase(unittest.TestCase): test_object = TestObjectFactory.build() self.assertEqual(test_object.one, 'one') - def testSelfAttribute(self): + def test_self_attribute(self): class TmpObj(object): n = 3 @@ -450,7 +450,7 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual(3, test_object.four) self.assertEqual(5, test_object.five) - def testSelfAttributeParent(self): + def test_self_attribute_parent(self): class TestModel2(FakeModel): pass @@ -467,7 +467,7 @@ class UsingFactoryTestCase(unittest.TestCase): test_model = TestModel2Factory() self.assertEqual(4, test_model.two.three) - def testSequenceDecorator(self): + def test_sequence_decorator(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -478,7 +478,7 @@ class UsingFactoryTestCase(unittest.TestCase): test_object = TestObjectFactory.build() self.assertEqual(test_object.one, 'one0') - def testLazyAttributeSequenceDecorator(self): + def test_lazy_attribute_sequence_decorator(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -493,7 +493,7 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual(test_object.one, 'one0') self.assertEqual(test_object.two, 'one0 two0') - def testBuildWithParameters(self): + def test_build_with_parameters(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -509,7 +509,7 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual(test_object1.one, 'other') self.assertEqual(test_object1.two, 'two1') - def testCreate(self): + def test_create(self): class TestModelFactory(FakeModelFactory): FACTORY_FOR = TestModel @@ -684,7 +684,7 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual('%d two' % i, obj.two) self.assertEqual(i, obj.three) - def testInheritance(self): + def test_inheritance(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -706,7 +706,7 @@ class UsingFactoryTestCase(unittest.TestCase): test_object_alt = TestObjectFactory.build() self.assertEqual(None, test_object_alt.three) - def testInheritanceWithInheritedClass(self): + def test_inheritance_with_inherited_class(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -723,7 +723,7 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual(test_object.three, 'three') self.assertEqual(test_object.four, 'three four') - def testDualInheritance(self): + def test_dual_inheritance(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -743,7 +743,7 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual('three', obj.three) self.assertEqual('four', obj.four) - def testClassMethodAccessible(self): + def test_class_method_accessible(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -753,7 +753,7 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual(TestObjectFactory.alt_create(foo=1), {"foo": 1}) - def testStaticMethodAccessible(self): + def test_static_method_accessible(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject @@ -895,7 +895,7 @@ class KwargAdjustTestCase(unittest.TestCase): class SubFactoryTestCase(unittest.TestCase): - def testSubFactory(self): + def test_sub_factory(self): class TestModel2(FakeModel): pass @@ -912,7 +912,7 @@ class SubFactoryTestCase(unittest.TestCase): self.assertEqual(1, test_model.id) self.assertEqual(1, test_model.two.id) - def testSubFactoryWithLazyFields(self): + def test_sub_factory_with_lazy_fields(self): class TestModel2(FakeModel): pass @@ -930,7 +930,7 @@ class SubFactoryTestCase(unittest.TestCase): self.assertEqual('x0x', test_model.two.one) self.assertEqual('x0xx0x', test_model.two.two) - def testSubFactoryAndSequence(self): + def test_sub_factory_and_sequence(self): class TestObject(object): def __init__(self, **kwargs): for k, v in kwargs.items(): @@ -951,7 +951,7 @@ class SubFactoryTestCase(unittest.TestCase): wrapping = WrappingTestObjectFactory.build() self.assertEqual(1, wrapping.wrapped.one) - def testSubFactoryOverriding(self): + def test_sub_factory_overriding(self): class TestObject(object): def __init__(self, **kwargs): for k, v in kwargs.items(): @@ -978,7 +978,7 @@ class SubFactoryTestCase(unittest.TestCase): self.assertEqual(wrapping.wrapped.three, 3) self.assertEqual(wrapping.wrapped.four, 4) - def testNestedSubFactory(self): + def test_nested_sub_factory(self): """Test nested sub-factories.""" class TestObject(object): @@ -1004,7 +1004,7 @@ class SubFactoryTestCase(unittest.TestCase): self.assertEqual(outer.wrap.wrapped.two, 2) self.assertEqual(outer.wrap.wrapped_bis.one, 1) - def testNestedSubFactoryWithOverriddenSubFactories(self): + def test_nested_sub_factory_with_overridden_sub_factories(self): """Test nested sub-factories, with attributes overridden with subfactories.""" class TestObject(object): @@ -1032,7 +1032,7 @@ class SubFactoryTestCase(unittest.TestCase): self.assertEqual(outer.wrap.wrapped.two.four, 4) self.assertEqual(outer.wrap.friend, 5) - def testSubFactoryAndInheritance(self): + def test_sub_factory_and_inheritance(self): """Test inheriting from a factory with subfactories, overriding.""" class TestObject(object): def __init__(self, **kwargs): @@ -1056,7 +1056,7 @@ class SubFactoryTestCase(unittest.TestCase): self.assertEqual(wrapping.wrapped.two, 4) self.assertEqual(wrapping.friend, 5) - def testDiamondSubFactory(self): + def test_diamond_sub_factory(self): """Tests the case where an object has two fields with a common field.""" class InnerMost(object): def __init__(self, a, b): -- cgit v1.2.3 From 8c1784e8c1eac65f66b4a1ecc4b8b0ddd5de9327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Apr 2013 01:17:26 +0200 Subject: Pylint. --- .pylintrc | 240 +++++++++++++++++++++++++++++++++++++++++++++ Makefile | 18 +++- factory/__init__.py | 28 +++--- factory/base.py | 123 +++++++---------------- factory/compat.py | 10 +- factory/containers.py | 7 +- factory/declarations.py | 39 ++------ factory/helpers.py | 123 +++++++++++++++++++++++ tests/test_declarations.py | 3 +- 9 files changed, 445 insertions(+), 146 deletions(-) create mode 100644 .pylintrc create mode 100644 factory/helpers.py diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..e9f43c9 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,240 @@ +[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/Makefile b/Makefile index f9c27c0..274ee32 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,8 @@ +PACKAGE=factory +TESTS_DIR=tests +DOC_DIR=docs + + all: default @@ -11,14 +16,17 @@ clean: test: python -W default setup.py test +pylint: + pylint --rcfile=.pylintrc --report=no $(PACKAGE)/ + coverage: coverage erase - coverage run "--include=factory/*.py,tests/*.py" --branch setup.py test - coverage report "--include=factory/*.py,tests/*.py" - coverage html "--include=factory/*.py,tests/*.py" + 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" doc: - $(MAKE) -C docs html + $(MAKE) -C $(DOC_DIR) html -.PHONY: all default clean coverage doc test +.PHONY: all default clean coverage doc pylint test diff --git a/factory/__init__.py b/factory/__init__.py index adcf9c9..beb422e 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -28,19 +28,6 @@ from .base import ( StubFactory, DjangoModelFactory, - build, - create, - stub, - generate, - simple_generate, - make_factory, - - build_batch, - create_batch, - stub_batch, - generate_batch, - simple_generate_batch, - BUILD_STRATEGY, CREATE_STRATEGY, STUB_STRATEGY, @@ -58,6 +45,21 @@ from .declarations import ( PostGeneration, PostGenerationMethodCall, RelatedFactory, +) + +from .helpers import ( + build, + create, + stub, + generate, + simple_generate, + make_factory, + + build_batch, + create_batch, + stub_batch, + generate_batch, + simple_generate_batch, lazy_attribute, iterator, diff --git a/factory/base.py b/factory/base.py index 71c2eb1..13a0623 100644 --- a/factory/base.py +++ b/factory/base.py @@ -20,10 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -import re -import sys -import warnings - from . import containers # Strategies @@ -68,7 +64,7 @@ class FactoryMetaClass(type): """Factory metaclass for handling ordered declarations.""" def __call__(cls, **kwargs): - """Override the default Factory() syntax to call the default build strategy. + """Override the default Factory() syntax to call the default strategy. Returns an instance of the associated class. """ @@ -80,10 +76,11 @@ class FactoryMetaClass(type): elif cls.FACTORY_STRATEGY == STUB_STRATEGY: return cls.stub(**kwargs) else: - raise UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format(cls.FACTORY_STRATEGY)) + raise UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format( + cls.FACTORY_STRATEGY)) @classmethod - def _discover_associated_class(cls, class_name, attrs, inherited=None): + def _discover_associated_class(mcs, class_name, attrs, inherited=None): """Try to find the class associated with this factory. In order, the following tests will be performed: @@ -104,8 +101,6 @@ class FactoryMetaClass(type): AssociatedClassError: If we were unable to associate this factory to a class. """ - own_associated_class = None - if FACTORY_CLASS_DECLARATION in attrs: return attrs[FACTORY_CLASS_DECLARATION] @@ -120,7 +115,7 @@ class FactoryMetaClass(type): class_name) @classmethod - def _extract_declarations(cls, bases, attributes): + def _extract_declarations(mcs, bases, attributes): """Extract declarations from a class definition. Args: @@ -141,7 +136,8 @@ class FactoryMetaClass(type): 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, {})) + declarations.update_with_public( + getattr(base, CLASS_ATTRIBUTE_DECLARATIONS, {})) # Import attributes from the class definition attributes = postgen_declarations.update_with_public(attributes) @@ -154,7 +150,7 @@ class FactoryMetaClass(type): return attributes - def __new__(cls, class_name, bases, attrs, extra_attrs=None): + def __new__(mcs, class_name, bases, attrs, extra_attrs=None): """Record attributes as a pattern for later instance construction. This is called when a new Factory subclass is defined; it will collect @@ -174,7 +170,8 @@ class FactoryMetaClass(type): """ parent_factories = get_factory_bases(bases) if not parent_factories: - return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs) + return super(FactoryMetaClass, mcs).__new__( + mcs, class_name, bases, attrs) is_abstract = attrs.pop('ABSTRACT_FACTORY', False) extra_attrs = {} @@ -185,7 +182,7 @@ class FactoryMetaClass(type): inherited_associated_class = getattr(base, CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None) - associated_class = cls._discover_associated_class(class_name, attrs, + associated_class = mcs._discover_associated_class(class_name, attrs, inherited_associated_class) # If inheriting the factory from a parent, keep a link to it. @@ -193,22 +190,23 @@ class FactoryMetaClass(type): if associated_class == inherited_associated_class: attrs['_base_factory'] = base - # The CLASS_ATTRIBUTE_ASSOCIATED_CLASS must *not* be taken into account - # when parsing the declared attributes of the new class. + # 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} # Extract pre- and post-generation declarations - attributes = cls._extract_declarations(parent_factories, attrs) + attributes = mcs._extract_declarations(parent_factories, attrs) # Add extra args if provided. if extra_attrs: attributes.update(extra_attrs) - return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attributes) + return super(FactoryMetaClass, mcs).__new__( + mcs, class_name, bases, attributes) - def __str__(self): - return '<%s for %s>' % (self.__name__, - getattr(self, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__) + def __str__(cls): + return '<%s for %s>' % (cls.__name__, + getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__) # Factory base classes @@ -216,6 +214,7 @@ class FactoryMetaClass(type): class BaseFactory(object): """Factory base support for sequences, attributes and stubs.""" + # Backwards compatibility UnknownStrategy = UnknownStrategy UnsupportedStrategy = UnsupportedStrategy @@ -231,6 +230,9 @@ class BaseFactory(object): # class. _base_factory = None + # Holds the target class, once resolved. + _associated_class = None + # List of arguments that should be passed as *args instead of **kwargs FACTORY_ARG_PARAMETERS = () @@ -329,7 +331,8 @@ class BaseFactory(object): attrs (dict): attributes to use for generating the object """ # Extract declarations used for post-generation - postgen_declarations = getattr(cls, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS) + postgen_declarations = getattr(cls, + CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS) postgen_attributes = {} for name, decl in sorted(postgen_declarations.items()): postgen_attributes[name] = decl.extract(name, attrs) @@ -341,7 +344,8 @@ class BaseFactory(object): results = {} for name, decl in sorted(postgen_declarations.items()): extracted, extracted_kwargs = postgen_attributes[name] - results[name] = decl.call(obj, create, extracted, **extracted_kwargs) + results[name] = decl.call(obj, create, extracted, + **extracted_kwargs) cls._after_postgeneration(obj, create, results) @@ -527,7 +531,7 @@ Factory = FactoryMetaClass('Factory', (BaseFactory,), { # Backwards compatibility -Factory.AssociatedClassError = AssociatedClassError +Factory.AssociatedClassError = AssociatedClassError # pylint: disable=W0201 class StubFactory(Factory): @@ -547,7 +551,7 @@ class StubFactory(Factory): class DjangoModelFactory(Factory): """Factory for Django models. - This makes sure that the 'sequence' field of created objects is an unused id. + This makes sure that the 'sequence' field of created objects is a new id. Possible improvement: define a new 'attribute' type, AutoField, which would handle those for non-numerical primary keys. @@ -559,7 +563,7 @@ class DjangoModelFactory(Factory): @classmethod def _get_manager(cls, target_class): try: - return target_class._default_manager + return target_class._default_manager # pylint: disable=W0212 except AttributeError: return target_class.objects @@ -567,7 +571,8 @@ class DjangoModelFactory(Factory): def _setup_next_sequence(cls): """Compute the next available PK, based on the 'pk' database field.""" - manager = cls._get_manager(cls._associated_class) + model = cls._associated_class # pylint: disable=E1101 + manager = cls._get_manager(model) try: return 1 + manager.values_list('pk', flat=True @@ -590,7 +595,7 @@ class DjangoModelFactory(Factory): key_fields[field] = kwargs.pop(field) key_fields['defaults'] = kwargs - obj, created = manager.get_or_create(*args, **key_fields) + obj, _created = manager.get_or_create(*args, **key_fields) return obj @classmethod @@ -610,68 +615,6 @@ class MogoFactory(Factory): return target_class.new(*args, **kwargs) -def make_factory(klass, **kwargs): - """Create a new, simple factory for the given class.""" - factory_name = '%sFactory' % klass.__name__ - kwargs[FACTORY_CLASS_DECLARATION] = klass - base_class = kwargs.pop('FACTORY_CLASS', Factory) - - factory_class = type(Factory).__new__(type(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 - - -def build(klass, **kwargs): - """Create a factory for the given class, and build an instance.""" - return make_factory(klass, **kwargs).build() - - -def build_batch(klass, size, **kwargs): - """Create a factory for the given class, and build a batch of instances.""" - return make_factory(klass, **kwargs).build_batch(size) - - -def create(klass, **kwargs): - """Create a factory for the given class, and create an instance.""" - return make_factory(klass, **kwargs).create() - - -def create_batch(klass, size, **kwargs): - """Create a factory for the given class, and create a batch of instances.""" - return make_factory(klass, **kwargs).create_batch(size) - - -def stub(klass, **kwargs): - """Create a factory for the given class, and stub an instance.""" - return make_factory(klass, **kwargs).stub() - - -def stub_batch(klass, size, **kwargs): - """Create a factory for the given class, and stub a batch of instances.""" - return make_factory(klass, **kwargs).stub_batch(size) - - -def generate(klass, strategy, **kwargs): - """Create a factory for the given class, and generate an instance.""" - return make_factory(klass, **kwargs).generate(strategy) - - -def generate_batch(klass, strategy, size, **kwargs): - """Create a factory for the given class, and generate instances.""" - return make_factory(klass, **kwargs).generate_batch(strategy, size) - - -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) - - -def simple_generate_batch(klass, create, size, **kwargs): - """Create a factory for the given class, and simple_generate instances.""" - return make_factory(klass, **kwargs).simple_generate_batch(create, size) - - def use_strategy(new_strategy): """Force the use of a different strategy. diff --git a/factory/compat.py b/factory/compat.py index a924de0..84f31b7 100644 --- a/factory/compat.py +++ b/factory/compat.py @@ -25,9 +25,11 @@ import sys -is_python2 = (sys.version_info[0] == 2) +PY2 = (sys.version_info[0] == 2) -if is_python2: - string_types = (str, unicode) +if PY2: + def is_string(obj): + return isinstance(obj, (str, unicode)) else: - string_types = (str,) + def is_string(obj): + return isinstance(obj, str) diff --git a/factory/containers.py b/factory/containers.py index dc3a457..e02f9f9 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -228,9 +228,12 @@ class AttributeBuilder(object): self._containers = extra.pop('__containers', ()) self._attrs = factory.declarations(extra) - attrs_with_subfields = [k for k, v in self._attrs.items() if self.has_subfields(v)] + attrs_with_subfields = [ + k for k, v in self._attrs.items() + if self.has_subfields(v)] - self._subfields = utils.multi_extract_dict(attrs_with_subfields, self._attrs) + self._subfields = utils.multi_extract_dict( + attrs_with_subfields, self._attrs) def has_subfields(self, value): return isinstance(value, declarations.SubFactory) diff --git a/factory/declarations.py b/factory/declarations.py index 15d8d5b..3d76960 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -21,9 +21,7 @@ # THE SOFTWARE. -import collections import itertools -import warnings from . import compat from . import utils @@ -179,7 +177,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): + def __init__(self, function, type=int): # pylint: disable=W0622 super(Sequence, self).__init__() self.function = function self.type = type @@ -318,7 +316,7 @@ class SubFactory(ParameteredAttribute): self.factory_module = self.factory_name = '' else: # Must be a string - if not isinstance(factory, compat.string_types) or '.' not in factory: + if not (compat.is_string(factory) and '.' in factory): raise ValueError( "The argument of a SubFactory must be either a class " "or the fully qualified path to a Factory class; got " @@ -330,7 +328,8 @@ class SubFactory(ParameteredAttribute): """Retrieve the wrapped factory.Factory subclass.""" if self.factory is None: # Must be a module path - self.factory = utils.import_object(self.factory_module, self.factory_name) + self.factory = utils.import_object( + self.factory_module, self.factory_name) return self.factory def generate(self, create, params): @@ -390,10 +389,6 @@ class PostGeneration(PostGenerationDeclaration): return self.function(obj, create, extracted, **kwargs) -def post_generation(fun): - return PostGeneration(fun) - - class RelatedFactory(PostGenerationDeclaration): """Calls a factory once the object has been generated. @@ -414,7 +409,7 @@ class RelatedFactory(PostGenerationDeclaration): self.factory_module = self.factory_name = '' else: # Must be a string - if not isinstance(factory, compat.string_types) or '.' not in factory: + if not (compat.is_string(factory) and '.' in factory): raise ValueError( "The argument of a SubFactory must be either a class " "or the fully qualified path to a Factory class; got " @@ -426,7 +421,8 @@ class RelatedFactory(PostGenerationDeclaration): """Retrieve the wrapped factory.Factory subclass.""" if self.factory is None: # Must be a module path - self.factory = utils.import_object(self.factory_module, self.factory_name) + self.factory = utils.import_object( + self.factory_module, self.factory_name) return self.factory def call(self, obj, create, extracted=None, **kwargs): @@ -450,7 +446,7 @@ class PostGenerationMethodCall(PostGenerationDeclaration): Example: class UserFactory(factory.Factory): ... - password = factory.PostGenerationMethodCall('set_password', password='') + password = factory.PostGenerationMethodCall('set_pass', password='') """ def __init__(self, method_name, *args, **kwargs): super(PostGenerationMethodCall, self).__init__() @@ -472,22 +468,3 @@ class PostGenerationMethodCall(PostGenerationDeclaration): passed_kwargs.update(kwargs) method = getattr(obj, self.method_name) method(*passed_args, **passed_kwargs) - - -# Decorators... in case lambdas don't cut it - -def lazy_attribute(func): - return LazyAttribute(func) - -def iterator(func): - """Turn a generator function into an iterator attribute.""" - return Iterator(func()) - -def sequence(func): - return Sequence(func) - -def lazy_attribute_sequence(func): - return LazyAttributeSequence(func) - -def container_attribute(func): - return ContainerAttribute(func, strict=False) diff --git a/factory/helpers.py b/factory/helpers.py new file mode 100644 index 0000000..8f0d161 --- /dev/null +++ b/factory/helpers.py @@ -0,0 +1,123 @@ +# -*- 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. + + +"""Simple wrappers around Factory class definition.""" + + +from . import base +from . import declarations + + +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 + base_class = kwargs.pop('FACTORY_CLASS', base.Factory) + + 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 + + +def build(klass, **kwargs): + """Create a factory for the given class, and build an instance.""" + return make_factory(klass, **kwargs).build() + + +def build_batch(klass, size, **kwargs): + """Create a factory for the given class, and build a batch of instances.""" + return make_factory(klass, **kwargs).build_batch(size) + + +def create(klass, **kwargs): + """Create a factory for the given class, and create an instance.""" + return make_factory(klass, **kwargs).create() + + +def create_batch(klass, size, **kwargs): + """Create a factory for the given class, and create a batch of instances.""" + return make_factory(klass, **kwargs).create_batch(size) + + +def stub(klass, **kwargs): + """Create a factory for the given class, and stub an instance.""" + return make_factory(klass, **kwargs).stub() + + +def stub_batch(klass, size, **kwargs): + """Create a factory for the given class, and stub a batch of instances.""" + return make_factory(klass, **kwargs).stub_batch(size) + + +def generate(klass, strategy, **kwargs): + """Create a factory for the given class, and generate an instance.""" + return make_factory(klass, **kwargs).generate(strategy) + + +def generate_batch(klass, strategy, size, **kwargs): + """Create a factory for the given class, and generate instances.""" + 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) + + +def simple_generate_batch(klass, create, size, **kwargs): + """Create a factory for the given class, and simple_generate instances.""" + return make_factory(klass, **kwargs).simple_generate_batch(create, size) + + +# pylint: enable=W0621 + + +def lazy_attribute(func): + return declarations.LazyAttribute(func) + + +def iterator(func): + """Turn a generator function into an iterator attribute.""" + return declarations.Iterator(func()) + + +def sequence(func): + return declarations.Sequence(func) + + +def lazy_attribute_sequence(func): + return declarations.LazyAttributeSequence(func) + + +def container_attribute(func): + return declarations.ContainerAttribute(func, strict=False) + + +def post_generation(fun): + return declarations.PostGeneration(fun) diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 7b9b0af..4c08dfa 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -25,6 +25,7 @@ import itertools import warnings from factory import declarations +from factory import helpers from .compat import mock, unittest from . import tools @@ -124,7 +125,7 @@ class PostGenerationDeclarationTestCase(unittest.TestCase): def test_decorator_simple(self): call_params = [] - @declarations.post_generation + @helpers.post_generation def foo(*args, **kwargs): call_params.append(args) call_params.append(kwargs) -- cgit v1.2.3 From 54971381fb31167d1f47b5e705480e044d702602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 11 Apr 2013 23:59:32 +0200 Subject: Add factory.fuzzy (Closes #41). --- README | 1 + docs/changelog.rst | 3 +- docs/fuzzy.rst | 88 ++++++++++++++++++++++++++++++++++++++++++++ factory/fuzzy.py | 86 +++++++++++++++++++++++++++++++++++++++++++ tests/__init__.py | 1 + tests/test_fuzzy.py | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 docs/fuzzy.rst create mode 100644 factory/fuzzy.py create mode 100644 tests/test_fuzzy.py diff --git a/README b/README index 3756109..cc26087 100644 --- a/README +++ b/README @@ -221,6 +221,7 @@ Contents, indices and tables reference orms recipes + fuzzy examples internals changelog diff --git a/docs/changelog.rst b/docs/changelog.rst index 4b24797..100952c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,8 +4,6 @@ ChangeLog 2.0.0 (current) --------------- -.. note:: This section lists features planned for v2 of factory_boy. Changes announced here may not have been committed to the repository. - *New:* - Allow overriding the base factory class for :func:`~factory.make_factory` and friends. @@ -15,6 +13,7 @@ ChangeLog the associated class' constructor - Add support for ``get_or_create`` in :class:`~factory.DjangoModelFactory`, through :attr:`~factory.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE`. + - Add support for :mod:`~factory.fuzzy` attribute definitions. *Removed:* diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst new file mode 100644 index 0000000..f1f4085 --- /dev/null +++ b/docs/fuzzy.rst @@ -0,0 +1,88 @@ +Fuzzy attributes +================ + +.. module:: factory.fuzzy + +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. + + +FuzzyAttribute +-------------- + + +.. class:: FuzzyAttribute + + The :class:`FuzzyAttribute` uses an arbitrary callable as fuzzer. + It is expected that successive calls of that function return various + values. + + .. attribute:: fuzzer + + The callable that generates random values + + +FuzzyChoice +----------- + + +.. class:: FuzzyChoice(choices) + + 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. + + .. attribute:: choices + + The list of choices to select randomly + + +FuzzyInteger +------------ + +.. class:: FuzzyInteger(low[, high]) + + The :class:`FuzzyInteger` fuzzer generates random integers within a given + inclusive range. + + The :attr:`low` bound may be omitted, in which case it defaults to 0: + + .. code-block:: pycon + + >>> FuzzyInteger(0, 42) + >>> fi.low, fi.high + 0, 42 + + >>> fi = FuzzyInteger(42) + >>> fi.low, fi.high + 0, 42 + + .. attribute:: low + + int, the inclusive lower bound of generated integers + + .. attribute:: high + + int, the inclusive higher bound of generated integers + + +Custom fuzzy fields +------------------- + +Alternate fuzzy fields may be defined. +They should inherit from the :class:`BaseFuzzyAttribute` class, and override its +:meth:`~BaseFuzzyAttribute.fuzz` method. + + +.. class:: BaseFuzzyAttribute + + Base class for all fuzzy attributes. + + .. method:: fuzz(self) + + The method responsible for generating random values. + *Must* be overridden in subclasses. diff --git a/factory/fuzzy.py b/factory/fuzzy.py new file mode 100644 index 0000000..186b4a7 --- /dev/null +++ b/factory/fuzzy.py @@ -0,0 +1,86 @@ +# -*- 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. + + +"""Additional declarations for "fuzzy" attribute definitions.""" + + +import random + +from . import declarations + + +class BaseFuzzyAttribute(declarations.OrderedDeclaration): + """Base class for fuzzy attributes. + + Custom fuzzers should override the `fuzz()` method. + """ + + def fuzz(self): + raise NotImplementedError() + + def evaluate(self, sequence, obj, create, extra=None, containers=()): + return self.fuzz() + + +class FuzzyAttribute(BaseFuzzyAttribute): + """Similar to LazyAttribute, but yields random values. + + Attributes: + function (callable): function taking no parameters and returning a + random value. + """ + + def __init__(self, fuzzer, **kwargs): + super(FuzzyAttribute, self).__init__(**kwargs) + self.fuzzer = fuzzer + + def fuzz(self): + return self.fuzzer() + + +class FuzzyChoice(BaseFuzzyAttribute): + """Handles fuzzy choice of an attribute.""" + + def __init__(self, choices, **kwargs): + self.choices = list(choices) + super(FuzzyChoice, self).__init__(**kwargs) + + def fuzz(self): + return random.choice(self.choices) + + +class FuzzyInteger(BaseFuzzyAttribute): + """Random integer 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(FuzzyInteger, self).__init__(**kwargs) + + def fuzz(self): + return random.randint(self.low, self.high) diff --git a/tests/__init__.py b/tests/__init__.py index 7531edd..3c620d6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -4,5 +4,6 @@ from .test_base import * from .test_containers import * from .test_declarations import * +from .test_fuzzy import * from .test_using import * from .test_utils import * diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py new file mode 100644 index 0000000..70a2095 --- /dev/null +++ b/tests/test_fuzzy.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Mark Sandstrom +# Copyright (c) 2011-2013 Raphaël Barrois +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +from factory import fuzzy + +from .compat import mock, unittest + + +class FuzzyAttributeTestCase(unittest.TestCase): + def test_simple_call(self): + d = fuzzy.FuzzyAttribute(lambda: 10) + + res = d.evaluate(2, None, False) + self.assertEqual(10, res) + + res = d.evaluate(2, None, False) + self.assertEqual(10, res) + + +class FuzzyChoiceTestCase(unittest.TestCase): + def test_unbiased(self): + options = [1, 2, 3] + d = fuzzy.FuzzyChoice(options) + res = d.evaluate(2, None, False) + self.assertIn(res, options) + + def test_mock(self): + options = [1, 2, 3] + fake_choice = lambda d: sum(d) + + d = fuzzy.FuzzyChoice(options) + + with mock.patch('random.choice', fake_choice): + res = d.evaluate(2, None, False) + + self.assertEqual(6, res) + + def test_generator(self): + def options(): + for i in range(3): + yield i + + d = fuzzy.FuzzyChoice(options()) + + res = d.evaluate(2, None, False) + self.assertIn(res, [0, 1, 2]) + + # And repeat + res = d.evaluate(2, None, False) + self.assertIn(res, [0, 1, 2]) + + +class FuzzyIntegerTestCase(unittest.TestCase): + 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) + self.assertIn(res, [2, 3]) + + fuzz = fuzzy.FuzzyInteger(4) + for _i in range(20): + res = fuzz.evaluate(2, None, False) + self.assertIn(res, [0, 1, 2, 3, 4]) + + def test_biased(self): + fake_randint = lambda low, high: low + high + + fuzz = fuzzy.FuzzyInteger(2, 8) + + with mock.patch('random.randint', fake_randint): + res = fuzz.evaluate(2, None, False) + + self.assertEqual(10, res) + + def test_biased_high_only(self): + fake_randint = lambda low, high: low + high + + fuzz = fuzzy.FuzzyInteger(8) + + with mock.patch('random.randint', fake_randint): + res = fuzz.evaluate(2, None, False) + + self.assertEqual(8, res) -- cgit v1.2.3 From e7a9a87320c78ec05a5d548516fe17c258e6d4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 15 Apr 2013 02:21:08 +0200 Subject: Allow overriding the sequence counter. --- docs/changelog.rst | 1 + docs/reference.rst | 31 +++++++++++++++++++++++++++++++ factory/base.py | 8 +++++++- factory/containers.py | 13 +++++++++---- tests/test_using.py | 16 ++++++++++++++++ 5 files changed, 64 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 100952c..80074ae 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,6 +14,7 @@ ChangeLog - Add support for ``get_or_create`` in :class:`~factory.DjangoModelFactory`, through :attr:`~factory.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 *Removed:* diff --git a/docs/reference.rst b/docs/reference.rst index a2af327..13220b0 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -502,6 +502,37 @@ sequence counter is shared: '123-555-0003' +Forcing a sequence counter +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a specific value of the sequence counter is required for one instance, the +``__sequence`` keyword argument should be passed to the factory method. + +This will force the sequence counter during the call, without altering the +class-level value. + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + uid = factory.Sequence(int) + +.. code-block:: pycon + + >>> UserFactory() + + >>> UserFactory() + + >>> UserFactory(__sequence=42) + + + +.. warning:: The impact of setting ``__sequence=n`` on a ``_batch`` call is + undefined. Each generated instance may share a same counter, or + use incremental values starting from the forced value. + + LazyAttributeSequence """"""""""""""""""""" diff --git a/factory/base.py b/factory/base.py index 13a0623..25f4714 100644 --- a/factory/base.py +++ b/factory/base.py @@ -282,7 +282,13 @@ class BaseFactory(object): applicable; the current list of computed attributes is available to the currently processed object. """ - return containers.AttributeBuilder(cls, extra).build(create) + force_sequence = None + if extra: + force_sequence = extra.pop('__sequence', None) + return containers.AttributeBuilder(cls, extra).build( + create=create, + force_sequence=force_sequence, + ) @classmethod def declarations(cls, extra_defs=None): diff --git a/factory/containers.py b/factory/containers.py index e02f9f9..ee2ad82 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -236,16 +236,21 @@ class AttributeBuilder(object): attrs_with_subfields, self._attrs) def has_subfields(self, value): - return isinstance(value, declarations.SubFactory) + return isinstance(value, declarations.ParameteredAttribute) - def build(self, create): + 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. - self.factory.sequence = self.factory._generate_next_sequence() + if force_sequence is None: + sequence = self.factory._generate_next_sequence() + else: + sequence = force_sequence # Parse attribute declarations, wrapping SubFactory and # OrderedDeclaration. @@ -253,7 +258,7 @@ class AttributeBuilder(object): for k, v in self._attrs.items(): if isinstance(v, declarations.OrderedDeclaration): v = OrderedDeclarationWrapper(v, - sequence=self.factory.sequence, + sequence=sequence, create=create, extra=self._subfields.get(k, {}), ) diff --git a/tests/test_using.py b/tests/test_using.py index dde0ba7..def49e4 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -346,6 +346,22 @@ class UsingFactoryTestCase(unittest.TestCase): self.assertEqual('one43', test_object1.one) self.assertEqual('two43', test_object1.two) + def test_sequence_override(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + + one = factory.Sequence(lambda n: 'one%d' % n) + + o1 = TestObjectFactory() + o2 = TestObjectFactory() + o3 = TestObjectFactory(__sequence=42) + o4 = TestObjectFactory() + + self.assertEqual('one0', o1.one) + self.assertEqual('one1', o2.one) + self.assertEqual('one42', o3.one) + self.assertEqual('one2', o4.one) + def test_custom_create(self): class TestModelFactory(factory.Factory): FACTORY_FOR = TestModel -- cgit v1.2.3 From 2b661e6eae3187c05c4eb8e1c3790cee6a9e3032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 15 Apr 2013 02:22:01 +0200 Subject: Add Dict/List declarations (Closes #18). --- docs/changelog.rst | 1 + docs/reference.rst | 75 ++++++++++++++++++++++ factory/__init__.py | 7 ++ factory/base.py | 42 ++++++++++++ factory/declarations.py | 35 +++++++++- tests/test_base.py | 10 +++ tests/test_using.py | 166 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 333 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 80074ae..af43ea5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,7 @@ ChangeLog through :attr:`~factory.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE`. - Add support for :mod:`~factory.fuzzy` attribute definitions. - The :class:`Sequence` counter can be overridden when calling a generating function + - Add :class:`~factory.Dict` and :class:`~factory.List` declarations (Closes #18). *Removed:* diff --git a/docs/reference.rst b/docs/reference.rst index 13220b0..81aa645 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -879,6 +879,81 @@ use the :func:`iterator` decorator: yield line +Dict and List +""""""""""""" + +When a factory expects lists or dicts as arguments, such values can be generated +through the whole range of factory_boy declarations, +with the :class:`Dict` and :class:`List` attributes: + +.. class:: Dict(params[, dict_factory=factory.DictFactory]) + + The :class:`Dict` class is used for dict-like attributes. + It receives as non-keyword argument a dictionary of fields to define, whose + value may be any factory-enabled declarations: + + .. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + is_superuser = False + roles = factory.Dict({ + 'role1': True, + 'role2': False, + 'role3': factory.Iterator([True, False]), + 'admin': factory.SelfAttribute('..is_superuser'), + }) + + .. note:: Declarations used as a :class:`Dict` values are evaluated within + that :class:`Dict`'s context; this means that you must use + the ``..foo`` syntax to access fields defined at the factory level. + + On the other hand, the :class:`Sequence` counter is aligned on the + containing factory's one. + + + The :class:`Dict` behaviour can be tuned through the following parameters: + + .. attribute:: dict_factory + + The actual factory to use for generating the dict can be set as a keyword + argument, if an exotic dictionary-like object (SortedDict, ...) is required. + + +.. class:: List(items[, list_factory=factory.ListFactory]) + + The :class:`List` can be used for list-like attributes. + + Internally, the fields are converted into a ``index=value`` dict, which + makes it possible to override some values at use time: + + .. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = User + + flags = factory.List([ + 'user', + 'active', + 'admin', + ]) + + .. code-block:: pycon + + >>> u = UserFactory(flags__2='superadmin') + >>> u.flags + ['user', 'active', 'superadmin'] + + + The :class:`List` behaviour can be tuned through the following parameters: + + .. attribute:: list_factory + + The actual factory to use for generating the list can be set as a keyword + argument, if another type (tuple, set, ...) is required. + + Post-generation hooks """"""""""""""""""""" diff --git a/factory/__init__.py b/factory/__init__.py index beb422e..9843fa1 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -25,6 +25,11 @@ __author__ = 'Raphaël Barrois ' from .base import ( Factory, + BaseDictFactory, + DictFactory, + BaseListFactory, + ListFactory, + MogoFactory, StubFactory, DjangoModelFactory, @@ -42,6 +47,8 @@ from .declarations import ( SelfAttribute, ContainerAttribute, SubFactory, + Dict, + List, PostGeneration, PostGenerationMethodCall, RelatedFactory, diff --git a/factory/base.py b/factory/base.py index 25f4714..928ea7a 100644 --- a/factory/base.py +++ b/factory/base.py @@ -621,6 +621,48 @@ class MogoFactory(Factory): return target_class.new(*args, **kwargs) +class BaseDictFactory(Factory): + """Factory for dictionary-like classes.""" + ABSTRACT_FACTORY = True + + @classmethod + def _build(cls, target_class, *args, **kwargs): + if args: + raise ValueError( + "DictFactory %r does not support FACTORY_ARG_PARAMETERS.", cls) + return target_class(**kwargs) + + @classmethod + def _create(cls, target_class, *args, **kwargs): + return cls._build(target_class, *args, **kwargs) + + +class DictFactory(BaseDictFactory): + FACTORY_FOR = dict + + +class BaseListFactory(Factory): + """Factory for list-like classes.""" + ABSTRACT_FACTORY = True + + @classmethod + def _build(cls, target_class, *args, **kwargs): + if args: + raise ValueError( + "ListFactory %r does not support FACTORY_ARG_PARAMETERS.", cls) + + values = [v for k, v in sorted(kwargs.items())] + return target_class(values) + + @classmethod + def _create(cls, target_class, *args, **kwargs): + return cls._build(target_class, *args, **kwargs) + + +class ListFactory(BaseListFactory): + FACTORY_FOR = list + + def use_strategy(new_strategy): """Force the use of a different strategy. diff --git a/factory/declarations.py b/factory/declarations.py index 3d76960..974b4ac 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -281,12 +281,14 @@ class ParameteredAttribute(OrderedDeclaration): containers = self._prepare_containers(obj, containers) defaults[self.CONTAINERS_FIELD] = containers - return self.generate(create, defaults) + return self.generate(sequence, obj, create, defaults) - def generate(self, create, params): # pragma: no cover + def generate(self, sequence, obj, create, params): # pragma: no cover """Actually generate the related attribute. Args: + sequence (int): the current sequence number + obj (LazyStub): the object being constructed create (bool): whether the calling factory was in 'create' or 'build' mode params (dict): parameters inherited from init and evaluation-time @@ -332,7 +334,7 @@ class SubFactory(ParameteredAttribute): self.factory_module, self.factory_name) return self.factory - def generate(self, create, params): + def generate(self, sequence, obj, create, params): """Evaluate the current definition and fill its attributes. Args: @@ -345,6 +347,33 @@ class SubFactory(ParameteredAttribute): return subfactory.simple_generate(create, **params) +class Dict(SubFactory): + """Fill a dict with usual declarations.""" + + 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() + return dict_factory.simple_generate(create, + __sequence=sequence, + **params) + + +class List(SubFactory): + """Fill a list with standard declarations.""" + + 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() + return list_factory.simple_generate(create, + __sequence=sequence, + **params) + + class PostGenerationDeclaration(object): """Declarations to be called once the target object has been generated.""" diff --git a/tests/test_base.py b/tests/test_base.py index 969ef13..73e59fa 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -63,6 +63,15 @@ class SafetyTestCase(unittest.TestCase): self.assertRaises(base.FactoryError, base.BaseFactory) +class AbstractFactoryTestCase(unittest.TestCase): + def test_factory_for_optional(self): + """Ensure that FACTORY_FOR is optional for ABSTRACT_FACTORY.""" + class TestObjectFactory(base.Factory): + ABSTRACT_FACTORY = True + + # Passed + + class FactoryTestCase(unittest.TestCase): def test_factory_for(self): class TestObjectFactory(base.Factory): @@ -106,6 +115,7 @@ class FactoryTestCase(unittest.TestCase): ones = set([x.one for x in (parent, alt_parent, sub, alt_sub)]) self.assertEqual(4, len(ones)) + class FactoryDefaultStrategyTestCase(unittest.TestCase): def setUp(self): self.default_strategy = base.Factory.FACTORY_STRATEGY diff --git a/tests/test_using.py b/tests/test_using.py index def49e4..c46bf2f 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1591,6 +1591,172 @@ class CircularTestCase(unittest.TestCase): self.assertIsNone(b.foo.bar.foo.bar) +class DictTestCase(unittest.TestCase): + def test_empty_dict(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + one = factory.Dict({}) + + o = TestObjectFactory() + self.assertEqual({}, o.one) + + def test_naive_dict(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + one = factory.Dict({'a': 1}) + + o = TestObjectFactory() + self.assertEqual({'a': 1}, o.one) + + def test_sequence_dict(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + one = factory.Dict({'a': factory.Sequence(lambda n: n + 2)}) + + o1 = TestObjectFactory() + o2 = TestObjectFactory() + + self.assertEqual({'a': 2}, o1.one) + self.assertEqual({'a': 3}, o2.one) + + def test_dict_override(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + one = factory.Dict({'a': 1}) + + o = TestObjectFactory(one__a=2) + self.assertEqual({'a': 2}, o.one) + + def test_dict_extra_key(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + one = factory.Dict({'a': 1}) + + o = TestObjectFactory(one__b=2) + self.assertEqual({'a': 1, 'b': 2}, o.one) + + def test_dict_merged_fields(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + two = 13 + one = factory.Dict({ + 'one': 1, + 'two': 2, + 'three': factory.SelfAttribute('two'), + }) + + o = TestObjectFactory(one__one=42) + self.assertEqual({'one': 42, 'two': 2, 'three': 2}, o.one) + + def test_nested_dicts(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + one = 1 + two = factory.Dict({ + 'one': 3, + 'two': factory.SelfAttribute('one'), + 'three': factory.Dict({ + 'one': 5, + 'two': factory.SelfAttribute('..one'), + 'three': factory.SelfAttribute('...one'), + }), + }) + + o = TestObjectFactory() + self.assertEqual(1, o.one) + self.assertEqual({ + 'one': 3, + 'two': 3, + 'three': { + 'one': 5, + 'two': 3, + 'three': 1, + }, + }, o.two) + + +class ListTestCase(unittest.TestCase): + def test_empty_list(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + one = factory.List([]) + + o = TestObjectFactory() + self.assertEqual([], o.one) + + def test_naive_list(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + one = factory.List([1]) + + o = TestObjectFactory() + self.assertEqual([1], o.one) + + def test_sequence_list(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + one = factory.List([factory.Sequence(lambda n: n + 2)]) + + o1 = TestObjectFactory() + o2 = TestObjectFactory() + + self.assertEqual([2], o1.one) + self.assertEqual([3], o2.one) + + def test_list_override(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + one = factory.List([1]) + + o = TestObjectFactory(one__0=2) + self.assertEqual([2], o.one) + + def test_list_extra_key(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + one = factory.List([1]) + + o = TestObjectFactory(one__1=2) + self.assertEqual([1, 2], o.one) + + def test_list_merged_fields(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + two = 13 + one = factory.List([ + 1, + 2, + factory.SelfAttribute('1'), + ]) + + o = TestObjectFactory(one__0=42) + self.assertEqual([42, 2, 2], o.one) + + def test_nested_lists(self): + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + one = 1 + two = factory.List([ + 3, + factory.SelfAttribute('0'), + factory.List([ + 5, + factory.SelfAttribute('..0'), + factory.SelfAttribute('...one'), + ]), + ]) + + o = TestObjectFactory() + self.assertEqual(1, o.one) + self.assertEqual([ + 3, + 3, + [ + 5, + 3, + 1, + ], + ], o.two) if __name__ == '__main__': unittest.main() -- cgit v1.2.3 From f35579bd37594b1d888e07db79bdd77a68f4f897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 15 Apr 2013 23:22:19 +0200 Subject: Release v2.0.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 af43ea5..d6d4bd8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,8 +1,8 @@ ChangeLog ========= -2.0.0 (current) ---------------- +2.0.0 (2013-04-15) +------------------ *New:* diff --git a/factory/__init__.py b/factory/__init__.py index 9843fa1..ef5d40e 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.0.0-dev' +__version__ = '2.0.0' __author__ = 'Raphaël Barrois ' from .base import ( -- cgit v1.2.3 From 9e17f7ef95f7951d7373d9f0f197dd21ac077725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 16 Apr 2013 08:10:51 +0200 Subject: Add more tests for DjangoModelFactoryTestCase. --- tests/test_using.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_using.py b/tests/test_using.py index c46bf2f..9e9e6aa 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1758,5 +1758,26 @@ class ListTestCase(unittest.TestCase): ], ], o.two) + +class DjangoModelFactoryTestCase(unittest.TestCase): + def test_sequence(self): + class TestModelFactory(factory.DjangoModelFactory): + FACTORY_FOR = TestModel + + a = factory.Sequence(lambda n: 'foo_%s' % n) + + o1 = TestModelFactory() + o2 = TestModelFactory() + + self.assertEqual('foo_2', o1.a) + self.assertEqual('foo_3', o2.a) + + o3 = TestModelFactory.build() + o4 = TestModelFactory.build() + + self.assertEqual('foo_4', o3.a) + self.assertEqual('foo_5', o4.a) + + if __name__ == '__main__': unittest.main() -- cgit v1.2.3 From 68b5872e8cbd33f5f59ea8d859e326eb0ff0c6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 16 Apr 2013 11:20:18 +0200 Subject: Release v2.0.1 --- docs/changelog.rst | 8 ++++++++ factory/__init__.py | 2 +- factory/base.py | 11 +++++++---- tests/test_using.py | 33 ++++++++++++++++++++++++++++++++- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d6d4bd8..76e2d43 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,14 @@ ChangeLog ========= +2.0.1 (2013-04-16) +------------------ + +*New:* + + - Don't push ``defaults`` to ``get_or_create`` when + :attr:`~factory.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` is not set. + 2.0.0 (2013-04-15) ------------------ diff --git a/factory/__init__.py b/factory/__init__.py index ef5d40e..939500c 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.0.0' +__version__ = '2.0.1' __author__ = 'Raphaël Barrois ' from .base import ( diff --git a/factory/base.py b/factory/base.py index 928ea7a..0d03838 100644 --- a/factory/base.py +++ b/factory/base.py @@ -596,10 +596,13 @@ class DjangoModelFactory(Factory): "(in %s.FACTORY_DJANGO_GET_OR_CREATE=%r)" % (cls, cls.FACTORY_DJANGO_GET_OR_CREATE)) - key_fields = {} - for field in cls.FACTORY_DJANGO_GET_OR_CREATE: - key_fields[field] = kwargs.pop(field) - key_fields['defaults'] = kwargs + if cls.FACTORY_DJANGO_GET_OR_CREATE: + key_fields = {} + for field in cls.FACTORY_DJANGO_GET_OR_CREATE: + key_fields[field] = kwargs.pop(field) + key_fields['defaults'] = kwargs + else: + key_fields = kwargs obj, _created = manager.get_or_create(*args, **key_fields) return obj diff --git a/tests/test_using.py b/tests/test_using.py index 9e9e6aa..d366c8c 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -50,9 +50,11 @@ class FakeModel(object): class FakeModelManager(object): def get_or_create(self, **kwargs): - kwargs.update(kwargs.pop('defaults', {})) + defaults = kwargs.pop('defaults', {}) + kwargs.update(defaults) instance = FakeModel.create(**kwargs) instance.id = 2 + instance._defaults = defaults return instance, True def values_list(self, *args, **kwargs): @@ -1778,6 +1780,35 @@ class DjangoModelFactoryTestCase(unittest.TestCase): self.assertEqual('foo_4', o3.a) self.assertEqual('foo_5', o4.a) + def test_no_get_or_create(self): + class TestModelFactory(factory.DjangoModelFactory): + FACTORY_FOR = TestModel + + a = factory.Sequence(lambda n: 'foo_%s' % n) + + o = TestModelFactory() + self.assertEqual({}, o._defaults) + self.assertEqual('foo_2', o.a) + self.assertEqual(2, o.id) + + def test_get_or_create(self): + class TestModelFactory(factory.DjangoModelFactory): + FACTORY_FOR = TestModel + FACTORY_DJANGO_GET_OR_CREATE = ('a', 'b') + + a = factory.Sequence(lambda n: 'foo_%s' % n) + b = 2 + c = 3 + d = 4 + + o = TestModelFactory() + self.assertEqual({'c': 3, 'd': 4}, o._defaults) + self.assertEqual('foo_2', o.a) + self.assertEqual(2, o.b) + self.assertEqual(3, o.c) + self.assertEqual(4, o.d) + self.assertEqual(2, o.id) + if __name__ == '__main__': unittest.main() -- cgit v1.2.3 From ef5011d7d28cc7034e07dc75a6661a0253c0fe1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 16 Apr 2013 11:32:36 +0200 Subject: Don't use objects.get_or_create() unless required. --- factory/base.py | 25 ++++++++++++++++--------- tests/test_using.py | 27 ++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/factory/base.py b/factory/base.py index 0d03838..2ff2944 100644 --- a/factory/base.py +++ b/factory/base.py @@ -587,8 +587,8 @@ class DjangoModelFactory(Factory): return 1 @classmethod - def _create(cls, target_class, *args, **kwargs): - """Create an instance of the model, and save it to the database.""" + 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, ( @@ -596,17 +596,24 @@ class DjangoModelFactory(Factory): "(in %s.FACTORY_DJANGO_GET_OR_CREATE=%r)" % (cls, cls.FACTORY_DJANGO_GET_OR_CREATE)) - if cls.FACTORY_DJANGO_GET_OR_CREATE: - key_fields = {} - for field in cls.FACTORY_DJANGO_GET_OR_CREATE: - key_fields[field] = kwargs.pop(field) - key_fields['defaults'] = kwargs - else: - key_fields = kwargs + key_fields = {} + for field in cls.FACTORY_DJANGO_GET_OR_CREATE: + key_fields[field] = kwargs.pop(field) + key_fields['defaults'] = kwargs obj, _created = manager.get_or_create(*args, **key_fields) return obj + @classmethod + def _create(cls, target_class, *args, **kwargs): + """Create an instance of the model, and save it to the database.""" + manager = cls._get_manager(target_class) + + if cls.FACTORY_DJANGO_GET_OR_CREATE: + return cls._get_or_create(target_class, *args, **kwargs) + + return manager.create(*args, **kwargs) + @classmethod def _after_postgeneration(cls, obj, create, results=None): """Save again the instance if creating and at least one hook ran.""" diff --git a/tests/test_using.py b/tests/test_using.py index d366c8c..821fad3 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -57,6 +57,12 @@ class FakeModel(object): instance._defaults = defaults return instance, True + def create(self, **kwargs): + instance = FakeModel.create(**kwargs) + instance.id = 2 + instance._defaults = None + return instance + def values_list(self, *args, **kwargs): return self @@ -1787,7 +1793,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase): a = factory.Sequence(lambda n: 'foo_%s' % n) o = TestModelFactory() - self.assertEqual({}, o._defaults) + self.assertEqual(None, o._defaults) self.assertEqual('foo_2', o.a) self.assertEqual(2, o.id) @@ -1809,6 +1815,25 @@ class DjangoModelFactoryTestCase(unittest.TestCase): self.assertEqual(4, o.d) self.assertEqual(2, o.id) + def test_full_get_or_create(self): + """Test a DjangoModelFactory with all fields in get_or_create.""" + class TestModelFactory(factory.DjangoModelFactory): + FACTORY_FOR = TestModel + FACTORY_DJANGO_GET_OR_CREATE = ('a', 'b', 'c', 'd') + + a = factory.Sequence(lambda n: 'foo_%s' % n) + b = 2 + c = 3 + d = 4 + + o = TestModelFactory() + self.assertEqual({}, o._defaults) + self.assertEqual('foo_2', o.a) + self.assertEqual(2, o.b) + self.assertEqual(3, o.c) + self.assertEqual(4, o.d) + self.assertEqual(2, o.id) + if __name__ == '__main__': unittest.main() -- cgit v1.2.3 From 876845102c4a217496d0f6435bfe1e3726d31fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 16 Apr 2013 11:33:00 +0200 Subject: Release v2.0.2 --- 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 76e2d43..173c40f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,15 @@ ChangeLog ========= +2.0.2 (2013-04-16) +------------------ + +*New:* + + - When :attr:`~factory.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` is + empty, use ``Model.objects.create()`` instead of ``Model.objects.get_or_create``. + + 2.0.1 (2013-04-16) ------------------ @@ -9,6 +18,7 @@ ChangeLog - Don't push ``defaults`` to ``get_or_create`` when :attr:`~factory.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` is not set. + 2.0.0 (2013-04-15) ------------------ diff --git a/factory/__init__.py b/factory/__init__.py index 939500c..e1138fa 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.0.1' +__version__ = '2.0.2' __author__ = 'Raphaël Barrois ' from .base import ( -- cgit v1.2.3