From 2b661e6eae3187c05c4eb8e1c3790cee6a9e3032 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois 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