summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/changelog.rst1
-rw-r--r--docs/reference.rst75
-rw-r--r--factory/__init__.py7
-rw-r--r--factory/base.py42
-rw-r--r--factory/declarations.py35
-rw-r--r--tests/test_base.py10
-rw-r--r--tests/test_using.py166
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 <raphael.barrois+fboy@polytechnique.org>'
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()