summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/changelog.rst11
-rw-r--r--docs/reference.rst11
-rw-r--r--factory/base.py57
-rw-r--r--factory/django.py2
-rw-r--r--tests/test_base.py24
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):