diff options
author | Raphaël Barrois <raphael.barrois@polyconseil.fr> | 2011-05-16 14:48:23 +0200 |
---|---|---|
committer | Raphaël Barrois <raphael.barrois@polyconseil.fr> | 2011-05-16 14:48:23 +0200 |
commit | f7cb0a040cab507541405d64efbf44b874acf207 (patch) | |
tree | 54f7eb2603bb21ff5e5410a3b634e6261763cae0 | |
parent | df8b1e10e06a6a6faa15808caa7167f3a8361c55 (diff) | |
download | factory-boy-f7cb0a040cab507541405d64efbf44b874acf207.tar factory-boy-f7cb0a040cab507541405d64efbf44b874acf207.tar.gz |
Add SubFactories, and full testing.
Signed-off-by: Raphaël Barrois <raphael.barrois@polyconseil.fr>
-rw-r--r-- | factory/__init__.py | 3 | ||||
-rw-r--r-- | factory/base.py | 12 | ||||
-rw-r--r-- | factory/containers.py | 48 | ||||
-rw-r--r-- | factory/declarations.py | 15 | ||||
-rw-r--r-- | factory/test_base.py | 63 | ||||
-rw-r--r-- | factory/test_containers.py | 212 | ||||
-rw-r--r-- | factory/test_declarations.py | 47 |
7 files changed, 369 insertions, 31 deletions
diff --git a/factory/__init__.py b/factory/__init__.py index bf0fca2..9a1513d 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -18,7 +18,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '1.0.0' # Remember to change in setup.py as well! +__version__ = '1.0.2' # Remember to change in setup.py as well! __author__ = 'Mark Sandstrom <mark@deliciouslynerdy.com>' from base import ( @@ -33,6 +33,7 @@ from declarations import ( LazyAttribute, Sequence, LazyAttributeSequence, + SubFactory, lazy_attribute, sequence, lazy_attribute_sequence diff --git a/factory/base.py b/factory/base.py index e78ec6e..733b130 100644 --- a/factory/base.py +++ b/factory/base.py @@ -153,7 +153,7 @@ class BaseFactory(object): return next_sequence @classmethod - def attributes(cls, **kwargs): + def attributes(cls, create=False, **kwargs): """Build a dict of attribute values, respecting declaration order. The process is: @@ -166,7 +166,11 @@ class BaseFactory(object): attributes = {} cls.sequence = cls._generate_next_sequence() - return getattr(cls, CLASS_ATTRIBUTE_DECLARATIONS).build_attributes(cls, kwargs) + return getattr(cls, CLASS_ATTRIBUTE_DECLARATIONS).build_attributes(cls, create, kwargs) + + @classmethod + def declarations(cls): + return DeclarationsHolder(getattr(cls, CLASS_ATTRIBUTE_DECLARATIONS)) @classmethod def build(cls, **kwargs): @@ -230,8 +234,8 @@ class Factory(BaseFactory): @classmethod def build(cls, **kwargs): - return cls._build(**cls.attributes(**kwargs)) + return cls._build(**cls.attributes(create=False, **kwargs)) @classmethod def create(cls, **kwargs): - return cls._create(**cls.attributes(**kwargs)) + return cls._create(**cls.attributes(create=True, **kwargs)) diff --git a/factory/containers.py b/factory/containers.py index 63be161..8aa9955 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -18,7 +18,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -from declarations import OrderedDeclaration +from declarations import OrderedDeclaration, SubFactory + + +ATTR_SPLITTER = '__' class ObjectParamsWrapper(object): '''A generic container that allows for getting but not setting of attributes. @@ -27,8 +30,8 @@ class ObjectParamsWrapper(object): initialized = False - def __init__(self, dict): - self.dict = dict + def __init__(self, attrs): + self.attrs = attrs self.initialized = True def __setattr__(self, name, value): @@ -39,7 +42,7 @@ class ObjectParamsWrapper(object): def __getattr__(self, name): try: - return self.dict[name] + return self.attrs[name] except KeyError: raise AttributeError("The param '{0}' does not exist. Perhaps your declarations are out of order?".format(name)) @@ -88,9 +91,12 @@ class OrderedDeclarationDict(object): class DeclarationsHolder(object): """Holds all declarations, ordered and unordered.""" - def __init__(self): + def __init__(self, defaults=None): + if not defaults: + defaults = {} self._ordered = OrderedDeclarationDict() self._unordered = {} + self.update_base(defaults) def update_base(self, attrs): """Updates the DeclarationsHolder from a class definition. @@ -119,10 +125,29 @@ class DeclarationsHolder(object): except KeyError: return self._ordered[key] - def build_attributes(self, factory, extra): + def iteritems(self): + for pair in self._unordered.iteritems(): + yield pair + for pair in self._ordered.iteritems(): + yield pair + + def items(self): + return list(self.iteritems()) + + def build_attributes(self, factory, create=False, extra=None): """Build the list of attributes based on class attributes.""" + if not extra: + extra = {} + attributes = {} - # For fields in _unordered, use the value from attrs if any; otherwise, + sub_fields = {} + for key in list(extra.keys()): + if ATTR_SPLITTER in key: + cls_name, attr_name = key.split(ATTR_SPLITTER, 1) + if cls_name in self: + sub_fields.setdefault(cls_name, {})[attr_name] = extra.pop(key) + + # For fields in _unordered, use the value from extra if any; otherwise, # use the default value. for key, value in self._unordered.iteritems(): attributes[key] = extra.get(key, value) @@ -130,8 +155,13 @@ class DeclarationsHolder(object): if key in extra: attributes[key] = extra[key] else: - wrapper = ObjectParamsWrapper(attributes) - attributes[key] = value.evaluate(factory, wrapper) + if isinstance(value, SubFactory): + new_value = value.evaluate(factory, create, + sub_fields.get(key, {})) + else: + wrapper = ObjectParamsWrapper(attributes) + new_value = value.evaluate(factory, wrapper) + attributes[key] = new_value attributes.update(extra) return attributes diff --git a/factory/declarations.py b/factory/declarations.py index 4c44e0c..a4a62af 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -75,6 +75,21 @@ class LazyAttributeSequence(Sequence): def evaluate(self, factory, attributes): return self.function(attributes, self.type(factory.sequence)) +class SubFactory(OrderedDeclaration): + """Base class for attributes based upon a sub-factory.""" + def __init__(self, factory, **kwargs): + super(SubFactory, self).__init__() + self.defaults = factory.declarations() + self.defaults.update_base(kwargs) + self.factory = factory + + def evaluate(self, factory, create, attributes): + attrs = self.defaults.build_attributes(self.factory, create, attributes) + if create: + return self.factory.create(**attrs) + else: + return self.factory.build(**attrs) + # Decorators... in case lambdas don't cut it def lazy_attribute(func): diff --git a/factory/test_base.py b/factory/test_base.py index ee0bc3c..aa8257b 100644 --- a/factory/test_base.py +++ b/factory/test_base.py @@ -20,6 +20,9 @@ import unittest +from base import BaseFactory, Factory, StubFactory, BUILD_STRATEGY, CREATE_STRATEGY, STUB_STRATEGY +import declarations + class TestObject(object): def __init__(self, one=None, two=None, three=None, four=None): self.one = one @@ -44,6 +47,12 @@ class FakeDjangoModel(object): class TestModel(FakeDjangoModel): pass + +class SafetyTestCase(unittest.TestCase): + def testBaseFactory(self): + self.assertRaises(RuntimeError, BaseFactory) + + class FactoryTestCase(unittest.TestCase): def testAttribute(self): class TestObjectFactory(Factory): @@ -54,8 +63,8 @@ class FactoryTestCase(unittest.TestCase): def testSequence(self): class TestObjectFactory(Factory): - one = Sequence(lambda n: 'one' + n) - two = Sequence(lambda n: 'two' + n) + one = declarations.Sequence(lambda n: 'one' + n) + two = declarations.Sequence(lambda n: 'two' + n) test_object0 = TestObjectFactory.build() self.assertEqual(test_object0.one, 'one0') @@ -67,8 +76,8 @@ class FactoryTestCase(unittest.TestCase): def testLazyAttribute(self): class TestObjectFactory(Factory): - one = LazyAttribute(lambda a: 'abc' ) - two = LazyAttribute(lambda a: a.one + ' xyz') + one = declarations.LazyAttribute(lambda a: 'abc' ) + two = declarations.LazyAttribute(lambda a: a.one + ' xyz') test_object = TestObjectFactory.build() self.assertEqual(test_object.one, 'abc') @@ -76,7 +85,7 @@ class FactoryTestCase(unittest.TestCase): def testLazyAttributeNonExistentParam(self): class TestObjectFactory(Factory): - one = LazyAttribute(lambda a: a.does_not_exist ) + one = declarations.LazyAttribute(lambda a: a.does_not_exist ) try: TestObjectFactory() @@ -86,8 +95,8 @@ class FactoryTestCase(unittest.TestCase): def testLazyAttributeSequence(self): class TestObjectFactory(Factory): - one = LazyAttributeSequence(lambda a, n: 'abc' + n) - two = LazyAttributeSequence(lambda a, n: a.one + ' xyz' + n) + one = declarations.LazyAttributeSequence(lambda a, n: 'abc' + n) + two = declarations.LazyAttributeSequence(lambda a, n: a.one + ' xyz' + n) test_object0 = TestObjectFactory.build() self.assertEqual(test_object0.one, 'abc0') @@ -99,7 +108,7 @@ class FactoryTestCase(unittest.TestCase): def testLazyAttributeDecorator(self): class TestObjectFactory(Factory): - @lazy_attribute + @declarations.lazy_attribute def one(a): return 'one' @@ -108,7 +117,7 @@ class FactoryTestCase(unittest.TestCase): def testSequenceDecorator(self): class TestObjectFactory(Factory): - @sequence + @declarations.sequence def one(n): return 'one' + n @@ -117,10 +126,10 @@ class FactoryTestCase(unittest.TestCase): def testLazyAttributeSequenceDecorator(self): class TestObjectFactory(Factory): - @lazy_attribute_sequence + @declarations.lazy_attribute_sequence def one(a, n): return 'one' + n - @lazy_attribute_sequence + @declarations.lazy_attribute_sequence def two(a, n): return a.one + ' two' + n @@ -130,8 +139,8 @@ class FactoryTestCase(unittest.TestCase): def testBuildWithParameters(self): class TestObjectFactory(Factory): - one = Sequence(lambda n: 'one' + n) - two = Sequence(lambda n: 'two' + n) + one = declarations.Sequence(lambda n: 'one' + n) + two = declarations.Sequence(lambda n: 'two' + n) test_object0 = TestObjectFactory.build(three='three') self.assertEqual(test_object0.one, 'one0') @@ -153,13 +162,13 @@ class FactoryTestCase(unittest.TestCase): def testInheritance(self): class TestObjectFactory(Factory): one = 'one' - two = LazyAttribute(lambda a: a.one + ' two') + two = declarations.LazyAttribute(lambda a: a.one + ' two') class TestObjectFactory2(TestObjectFactory): FACTORY_FOR = TestObject three = 'three' - four = LazyAttribute(lambda a: a.three + ' four') + four = declarations.LazyAttribute(lambda a: a.three + ' four') test_object = TestObjectFactory2.build() self.assertEqual(test_object.one, 'one') @@ -170,11 +179,11 @@ class FactoryTestCase(unittest.TestCase): def testInheritanceWithInheritedClass(self): class TestObjectFactory(Factory): one = 'one' - two = LazyAttribute(lambda a: a.one + ' two') + two = declarations.LazyAttribute(lambda a: a.one + ' two') class TestFactory(TestObjectFactory): three = 'three' - four = LazyAttribute(lambda a: a.three + ' four') + four = declarations.LazyAttribute(lambda a: a.three + ' four') test_object = TestFactory.build() self.assertEqual(test_object.one, 'one') @@ -221,6 +230,23 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): self.assertEqual(test_model.one, 'one') self.assertTrue(test_model.id) + def testSubFactory(self): + class TestModel2(FakeDjangoModel): + pass + + class TestModelFactory(Factory): + FACTORY_FOR = TestModel + one = 3 + + class TestModel2Factory(Factory): + FACTORY_FOR = TestModel2 + two = declarations.SubFactory(TestModelFactory, one=1) + + test_model = TestModel2Factory(two__one=4) + self.assertEqual(4, test_model.two.one) + self.assertEqual(1, test_model.id) + self.assertEqual(1, test_model.two.id) + def testStubStrategy(self): Factory.default_strategy = STUB_STRATEGY @@ -247,6 +273,9 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): self.assertRaises(StubFactory.UnsupportedStrategy, TestModelFactory) + TestModelFactory.default_strategy = BUILD_STRATEGY + self.assertRaises(StubFactory.UnsupportedStrategy, TestModelFactory) + class FactoryCreationTestCase(unittest.TestCase): def testFactoryFor(self): class TestFactory(Factory): diff --git a/factory/test_containers.py b/factory/test_containers.py new file mode 100644 index 0000000..3cc61e1 --- /dev/null +++ b/factory/test_containers.py @@ -0,0 +1,212 @@ +# Copyright (c) 2010 Mark Sandstrom +# +# 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 unittest + +from containers import DeclarationsHolder, OrderedDeclarationDict, ObjectParamsWrapper +import base +import declarations + +class ObjectParamsWrapperTestCase(unittest.TestCase): + def test_read(self): + vals = {'one': 1, 'two': 2} + wrapper = ObjectParamsWrapper(vals) + + self.assertEqual(1, wrapper.one) + self.assertEqual(2, wrapper.two) + self.assertRaises(AttributeError, getattr, wrapper, 'three') + self.assertRaises(AttributeError, setattr, wrapper, 'one', 1) + + def test_standard_attributes(self): + wrapper = ObjectParamsWrapper({}) + self.assertEqual(ObjectParamsWrapper, wrapper.__class__) + + +class OrderedDeclarationMock(object): + def __init__(self, order): + self.order = order + + +class OrderedDeclarationDictTestCase(unittest.TestCase): + def test_basics(self): + one = OrderedDeclarationMock(1) + two = OrderedDeclarationMock(2) + three = OrderedDeclarationMock(3) + d = OrderedDeclarationDict(one=one, two=two, three=three) + self.assertEqual(one, d['one']) + self.assertEqual(two, d['two']) + self.assertEqual(three, d['three']) + + self.assertTrue('one' in d) + self.assertTrue('two' in d) + self.assertTrue('three' in d) + + self.assertEqual(one, d.pop('one')) + self.assertFalse('one' in d) + + d['one'] = one + self.assertTrue('one' in d) + self.assertEqual(one, d['one']) + + self.assertEqual(set(['one', 'two', 'three']), + set(d)) + + def test_order(self): + one = OrderedDeclarationMock(1) + two = OrderedDeclarationMock(2) + ten = OrderedDeclarationMock(10) + d = OrderedDeclarationDict(one=one, two=two, ten=ten) + + self.assertEqual(['one', 'two', 'ten'], list(d)) + self.assertEqual([('one', one), ('two', two), ('ten', ten)], + d.items()) + self.assertEqual([('one', one), ('two', two), ('ten', ten)], + list(d.iteritems())) + + def test_insert(self): + one = OrderedDeclarationMock(1) + two = OrderedDeclarationMock(2) + four = OrderedDeclarationMock(4) + ten = OrderedDeclarationMock(10) + d = OrderedDeclarationDict(one=one, two=two, ten=ten) + + self.assertEqual(['one', 'two', 'ten'], list(d)) + d['four'] = four + self.assertEqual(['one', 'two', 'four', 'ten'], list(d)) + + def test_replace(self): + one = OrderedDeclarationMock(1) + two = OrderedDeclarationMock(2) + three = OrderedDeclarationMock(3) + ten = OrderedDeclarationMock(10) + d = OrderedDeclarationDict(one=one, two=two, ten=ten) + + self.assertEqual(['one', 'two', 'ten'], list(d)) + d['one'] = three + + self.assertEqual(three, d['one']) + self.assertEqual(['two', 'one', 'ten'], list(d)) + + +class DeclarationsHolderTestCase(unittest.TestCase): + def test_empty(self): + holder = DeclarationsHolder() + self.assertRaises(KeyError, holder.__getitem__, 'one') + holder.update_base({'one': 1}) + self.assertEqual(1, holder['one']) + + def test_simple(self): + """Tests a simple use case without OrderedDeclaration.""" + holder = DeclarationsHolder({'one': 1, 'two': 2}) + self.assertTrue('one' in holder) + self.assertTrue('two' in holder) + holder.update_base({'two': 3, 'three': 3}) + self.assertEqual(1, holder['one']) + self.assertEqual(3, holder['two']) + self.assertEqual(3, holder['three']) + + attrs = holder.build_attributes(None) + self.assertEqual(1, attrs['one']) + self.assertEqual(3, attrs['two']) + self.assertEqual(3, attrs['three']) + + self.assertEqual(set([('one', 1), ('two', 3), ('three', 3)]), + set(holder.items())) + + attrs = holder.build_attributes(None, False, {'two': 2}) + self.assertEqual(1, attrs['one']) + self.assertEqual(2, attrs['two']) + self.assertEqual(3, attrs['three']) + + def test_skip_specials(self): + """Makes sure that attributes starting with _ are skipped.""" + holder = DeclarationsHolder({'one': 1, '_two': 2}) + self.assertTrue('one' in holder) + self.assertFalse('_two' in holder) + + remains = holder.update_base({'_two': 2, 'three': 3}) + self.assertTrue('three' in holder) + self.assertFalse('_two' in holder) + self.assertEqual({'_two': 2}, remains) + + def test_ordered(self): + """Tests the handling of OrderedDeclaration.""" + two = declarations.LazyAttribute(lambda o: 2 * o.one) + three = declarations.LazyAttribute(lambda o: o.one + o.two) + holder = DeclarationsHolder({'one': 1, 'two': two, 'three': three}) + + self.assertEqual([('one', 1), ('two', two), ('three', three)], + holder.items()) + + self.assertEqual(two, holder['two']) + + attrs = holder.build_attributes(None) + self.assertEqual(1, attrs['one']) + self.assertEqual(2, attrs['two']) + self.assertEqual(3, attrs['three']) + + attrs = holder.build_attributes(None, False, {'one': 4}) + self.assertEqual(4, attrs['one']) + self.assertEqual(8, attrs['two']) + self.assertEqual(12, attrs['three']) + + attrs = holder.build_attributes(None, False, {'one': 4, 'two': 2}) + self.assertEqual(4, attrs['one']) + self.assertEqual(2, attrs['two']) + self.assertEqual(6, attrs['three']) + + def test_sub_factory(self): + """Tests the behaviour of sub-factories.""" + class TestObject(object): + def __init__(self, one=None, two=None, three=None, four=None): + self.one = one + self.two = two + self.three = three + self.four = four + + class TestObjectFactory(base.Factory): + FACTORY_FOR = TestObject + two = 2 + + sub = declarations.SubFactory(TestObjectFactory, + three=3, + four=declarations.LazyAttribute( + lambda o: 2 * o.two)) + + holder = DeclarationsHolder(defaults={'sub': sub, 'one': 1}) + self.assertEqual(sub, holder['sub']) + self.assertTrue('sub' in holder) + + attrs = holder.build_attributes(None) + self.assertEqual(1, attrs['one']) + self.assertEqual(2, attrs['sub'].two) + self.assertEqual(3, attrs['sub'].three) + self.assertEqual(4, attrs['sub'].four) + + attrs = holder.build_attributes(None, False, {'sub__two': 8, 'three__four': 4}) + self.assertEqual(1, attrs['one']) + self.assertEqual(4, attrs['three__four']) + self.assertEqual(8, attrs['sub'].two) + self.assertEqual(3, attrs['sub'].three) + self.assertEqual(16, attrs['sub'].four) + + +if __name__ == '__main__': + unittest.main() diff --git a/factory/test_declarations.py b/factory/test_declarations.py new file mode 100644 index 0000000..27d5f9e --- /dev/null +++ b/factory/test_declarations.py @@ -0,0 +1,47 @@ +# Copyright (c) 2010 Mark Sandstrom +# +# 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 unittest + +from declarations import GlobalCounter, OrderedDeclaration, Sequence + +class GlobalCounterTestCase(unittest.TestCase): + def test_incr(self): + init = GlobalCounter.step() + mid = GlobalCounter.step() + last = GlobalCounter.step() + self.assertEqual(2, last - init) + self.assertEqual(1, mid - init) + + +class OrderedDeclarationTestCase(unittest.TestCase): + def test_errors(self): + decl = OrderedDeclaration() + self.assertRaises(NotImplementedError, decl.evaluate, None, {}) + + def test_order(self): + decl1 = OrderedDeclaration() + decl2 = OrderedDeclaration() + decl3 = Sequence(lambda n: 3 * n) + self.assertTrue(decl1.order < decl2.order) + self.assertTrue(decl2.order < decl3.order) + +if __name__ == '__main__': + unittest.main() |