diff options
author | Raphaël Barrois <raphael.barrois@polytechnique.org> | 2013-08-13 12:06:52 +0200 |
---|---|---|
committer | Raphaël Barrois <raphael.barrois@polytechnique.org> | 2013-08-13 12:11:09 +0200 |
commit | 2796de70d5bed4cfff5749085ce4e6f16eba1b1e (patch) | |
tree | 71fbfd55fdebfce3faf44426731d7dd7bbf78efd | |
parent | 5730d4ac18f8684c37168033ef32d1ee31f5e4a1 (diff) | |
download | factory-boy-2796de70d5bed4cfff5749085ce4e6f16eba1b1e.tar factory-boy-2796de70d5bed4cfff5749085ce4e6f16eba1b1e.tar.gz |
Make ABSTRACT_FACTORY optional (Closes #74)
It will be automatically set to True if neither the Factory subclass nor
its parents define a FACTORY_FOR argument.
It can also be set on a Factory subclass to prevent it from being
called.
-rw-r--r-- | docs/changelog.rst | 11 | ||||
-rw-r--r-- | docs/reference.rst | 11 | ||||
-rw-r--r-- | factory/base.py | 57 | ||||
-rw-r--r-- | factory/django.py | 2 | ||||
-rw-r--r-- | tests/test_base.py | 24 |
5 files changed, 68 insertions, 37 deletions
diff --git a/docs/changelog.rst b/docs/changelog.rst index f9302ad..ec47b14 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,17 @@ ChangeLog ========= +.. _v2.1.2: + +2.1.2 (current) +--------------- + +*New:* + + - The :class:`~factory.Factory.ABSTRACT_FACTORY` keyword is now optional, and automatically set + to ``True`` if neither the :class:`~factory.Factory` subclass nor its parent declare the + :class:`~factory.Factory.FACTORY_FOR` attribute (:issue:`74`) + .. _v2.1.1: diff --git a/docs/reference.rst b/docs/reference.rst index e98665f..3d3097d 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -19,15 +19,18 @@ The :class:`Factory` class .. 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`. + This optional attribute describes the class of objects to generate. + + If unset, it will be inherited from parent :class:`Factory` subclasses. .. attribute:: ABSTRACT_FACTORY This attribute indicates that the :class:`Factory` subclass should not be used to generate objects, but instead provides some extra defaults. + It will be automatically set to ``True`` if neither the :class:`Factory` + subclass nor its parents define the :attr:`~Factory.FACTORY_FOR` attribute. + .. attribute:: FACTORY_ARG_PARAMETERS Some factories require non-keyword arguments to their :meth:`~object.__init__`. @@ -211,7 +214,7 @@ The :class:`Factory` class .. code-block:: python class BaseBackendFactory(factory.Factory): - ABSTRACT_FACTORY = True + ABSTRACT_FACTORY = True # Optional def _create(cls, target_class, *args, **kwargs): obj = target_class(*args, **kwargs) diff --git a/factory/base.py b/factory/base.py index 029185b..0db8249 100644 --- a/factory/base.py +++ b/factory/base.py @@ -40,6 +40,7 @@ FACTORY_CLASS_DECLARATION = 'FACTORY_FOR' CLASS_ATTRIBUTE_DECLARATIONS = '_declarations' CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS = '_postgen_declarations' CLASS_ATTRIBUTE_ASSOCIATED_CLASS = '_associated_class' +CLASS_ATTRIBUTE_IS_ABSTRACT = '_abstract_factory' class FactoryError(Exception): @@ -101,10 +102,6 @@ class FactoryMetaClass(type): Returns: class: the class to associate with this factory - - Raises: - AssociatedClassError: If we were unable to associate this factory - to a class. """ if FACTORY_CLASS_DECLARATION in attrs: return attrs[FACTORY_CLASS_DECLARATION] @@ -114,10 +111,8 @@ class FactoryMetaClass(type): if inherited is not None: return inherited - raise AssociatedClassError( - "Could not determine the class associated with %s. " - "Use the FACTORY_FOR attribute to specify an associated class." % - class_name) + # Nothing found, return None. + return None @classmethod def _extract_declarations(mcs, bases, attributes): @@ -155,7 +150,7 @@ class FactoryMetaClass(type): return attributes - def __new__(mcs, class_name, bases, attrs, extra_attrs=None): + def __new__(mcs, class_name, bases, attrs): """Record attributes as a pattern for later instance construction. This is called when a new Factory subclass is defined; it will collect @@ -166,9 +161,6 @@ class FactoryMetaClass(type): 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 @@ -178,18 +170,20 @@ class FactoryMetaClass(type): return super(FactoryMetaClass, mcs).__new__( mcs, class_name, bases, attrs) - is_abstract = attrs.pop('ABSTRACT_FACTORY', False) extra_attrs = {} - if not is_abstract: + is_abstract = attrs.pop('ABSTRACT_FACTORY', False) - base = parent_factories[0] + base = parent_factories[0] + inherited_associated_class = getattr(base, + CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None) + associated_class = mcs._discover_associated_class(class_name, attrs, + inherited_associated_class) - inherited_associated_class = getattr(base, - CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None) - associated_class = mcs._discover_associated_class(class_name, attrs, - inherited_associated_class) + if associated_class is None: + is_abstract = True + else: # 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: @@ -197,21 +191,23 @@ class FactoryMetaClass(type): # 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} + extra_attrs[CLASS_ATTRIBUTE_ASSOCIATED_CLASS] = associated_class + + extra_attrs[CLASS_ATTRIBUTE_IS_ABSTRACT] = is_abstract # Extract pre- and post-generation declarations attributes = mcs._extract_declarations(parent_factories, attrs) - - # Add extra args if provided. - if extra_attrs: - attributes.update(extra_attrs) + attributes.update(extra_attrs) return super(FactoryMetaClass, mcs).__new__( mcs, class_name, bases, attributes) def __str__(cls): - return '<%s for %s>' % (cls.__name__, - getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__) + if cls._abstract_factory: + return '<%s (abstract)>' + else: + return '<%s for %s>' % (cls.__name__, + getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__) # Factory base classes @@ -238,6 +234,9 @@ class BaseFactory(object): # Holds the target class, once resolved. _associated_class = None + # Whether this factory is considered "abstract", thus uncallable. + _abstract_factory = False + # List of arguments that should be passed as *args instead of **kwargs FACTORY_ARG_PARAMETERS = () @@ -367,6 +366,12 @@ class BaseFactory(object): create (bool): whether to 'build' or 'create' the object attrs (dict): attributes to use for generating the object """ + if cls._abstract_factory: + raise FactoryError( + "Cannot generate instances of abstract factory %(f)s; " + "Ensure %(f)s.FACTORY_FOR is set and %(f)s.ABSTRACT_FACTORY " + "is either not set or False." % dict(f=cls)) + # Extract declarations used for post-generation postgen_declarations = getattr(cls, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS) diff --git a/factory/django.py b/factory/django.py index 1672fcc..e3e8829 100644 --- a/factory/django.py +++ b/factory/django.py @@ -49,7 +49,7 @@ class DjangoModelFactory(base.Factory): handle those for non-numerical primary keys. """ - ABSTRACT_FACTORY = True + ABSTRACT_FACTORY = True # Optional, but explicit. FACTORY_DJANGO_GET_OR_CREATE = () @classmethod diff --git a/tests/test_base.py b/tests/test_base.py index 8ac2f44..8cea6fc 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -73,6 +73,20 @@ class AbstractFactoryTestCase(unittest.TestCase): # Passed + def test_factory_for_and_abstract_factory_optional(self): + """Ensure that ABSTRACT_FACTORY is optional.""" + class TestObjectFactory(base.Factory): + pass + + # passed + + def test_abstract_factory_cannot_be_called(self): + class TestObjectFactory(base.Factory): + pass + + self.assertRaises(base.FactoryError, TestObjectFactory.build) + self.assertRaises(base.FactoryError, TestObjectFactory.create) + class FactoryTestCase(unittest.TestCase): def test_factory_for(self): @@ -318,12 +332,10 @@ class FactoryCreationTestCase(unittest.TestCase): # Errors def test_no_associated_class(self): - try: - class Test(base.Factory): - pass - self.fail() # pragma: no cover - except base.Factory.AssociatedClassError as e: - self.assertTrue('autodiscovery' not in str(e)) + class Test(base.Factory): + pass + + self.assertTrue(Test._abstract_factory) class PostGenerationParsingTestCase(unittest.TestCase): |