summaryrefslogtreecommitdiff
path: root/factory/base.py
diff options
context:
space:
mode:
Diffstat (limited to 'factory/base.py')
-rw-r--r--factory/base.py636
1 files changed, 352 insertions, 284 deletions
diff --git a/factory/base.py b/factory/base.py
index 62131fb..2ff2944 100644
--- a/factory/base.py
+++ b/factory/base.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011 Raphaël Barrois
+# Copyright (c) 2011-2013 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -20,121 +20,71 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-import re
-import sys
-import warnings
-
-from factory import containers
+from . import containers
# Strategies
BUILD_STRATEGY = 'build'
CREATE_STRATEGY = 'create'
STUB_STRATEGY = 'stub'
-# Creation functions. Use Factory.set_creation_function() to set a creation function appropriate for your ORM.
-DJANGO_CREATION = lambda class_to_create, **kwargs: class_to_create.objects.create(**kwargs)
-
-# Building functions. Use Factory.set_building_function() to set a building functions appropriate for your ORM.
-NAIVE_BUILD = lambda class_to_build, **kwargs: class_to_build(**kwargs)
-MOGO_BUILD = lambda class_to_build, **kwargs: class_to_build.new(**kwargs)
-
# Special declarations
FACTORY_CLASS_DECLARATION = 'FACTORY_FOR'
# Factory class attributes
CLASS_ATTRIBUTE_DECLARATIONS = '_declarations'
+CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS = '_postgen_declarations'
CLASS_ATTRIBUTE_ASSOCIATED_CLASS = '_associated_class'
+class FactoryError(Exception):
+ """Any exception raised by factory_boy."""
+
+
+class AssociatedClassError(FactoryError):
+ """Exception for Factory subclasses lacking FACTORY_FOR."""
+
+
+class UnknownStrategy(FactoryError):
+ """Raised when a factory uses an unknown strategy."""
+
+
+class UnsupportedStrategy(FactoryError):
+ """Raised when trying to use a strategy on an incompatible Factory."""
+
+
# Factory metaclasses
def get_factory_bases(bases):
- """Retrieve all BaseFactoryMetaClass-derived bases from a list."""
- return [b for b in bases if isinstance(b, BaseFactoryMetaClass)]
+ """Retrieve all FactoryMetaClass-derived bases from a list."""
+ return [b for b in bases if issubclass(b, BaseFactory)]
-class BaseFactoryMetaClass(type):
+class FactoryMetaClass(type):
"""Factory metaclass for handling ordered declarations."""
def __call__(cls, **kwargs):
- """Override the default Factory() syntax to call the default build strategy.
+ """Override the default Factory() syntax to call the default strategy.
Returns an instance of the associated class.
"""
- if cls.default_strategy == BUILD_STRATEGY:
+ if cls.FACTORY_STRATEGY == BUILD_STRATEGY:
return cls.build(**kwargs)
- elif cls.default_strategy == CREATE_STRATEGY:
+ elif cls.FACTORY_STRATEGY == CREATE_STRATEGY:
return cls.create(**kwargs)
- elif cls.default_strategy == STUB_STRATEGY:
+ elif cls.FACTORY_STRATEGY == STUB_STRATEGY:
return cls.stub(**kwargs)
else:
- raise BaseFactory.UnknownStrategy('Unknown default_strategy: {0}'.format(cls.default_strategy))
-
- def __new__(cls, class_name, bases, attrs, extra_attrs=None):
- """Record attributes as a pattern for later instance construction.
-
- This is called when a new Factory subclass is defined; it will collect
- attribute declaration from the class definition.
-
- Args:
- class_name (str): the name of the class being created
- 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
- """
-
- parent_factories = get_factory_bases(bases)
- if not parent_factories or attrs.get('ABSTRACT_FACTORY', False):
- # If this isn't a subclass of Factory, or specifically declared
- # abstract, don't do anything special.
- return super(BaseFactoryMetaClass, cls).__new__(cls, class_name, bases, attrs)
-
- declarations = containers.DeclarationDict()
-
- # Add parent declarations in reverse order.
- for base in reversed(parent_factories):
- # Import all 'public' attributes (avoid those starting with _)
- declarations.update_with_public(getattr(base, CLASS_ATTRIBUTE_DECLARATIONS, {}))
-
- # Import attributes from the class definition, storing protected/private
- # attributes in 'non_factory_attrs'.
- non_factory_attrs = declarations.update_with_public(attrs)
-
- # Store the DeclarationDict in the attributes of the newly created class
- non_factory_attrs[CLASS_ATTRIBUTE_DECLARATIONS] = declarations
-
- # Add extra args if provided.
- if extra_attrs:
- non_factory_attrs.update(extra_attrs)
-
- return super(BaseFactoryMetaClass, cls).__new__(cls, class_name, bases, non_factory_attrs)
-
-
-class FactoryMetaClass(BaseFactoryMetaClass):
- """Factory metaclass for handling class association and ordered declarations."""
-
- ERROR_MESSAGE = """Could not determine what class this factory is for.
- Use the {0} attribute to specify a class."""
- ERROR_MESSAGE_AUTODISCOVERY = ERROR_MESSAGE + """
- Also, autodiscovery failed using the name '{1}'
- based on the Factory name '{2}' in {3}."""
+ raise UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format(
+ cls.FACTORY_STRATEGY))
@classmethod
- def _discover_associated_class(cls, class_name, attrs, inherited=None):
+ def _discover_associated_class(mcs, class_name, attrs, inherited=None):
"""Try to find the class associated with this factory.
In order, the following tests will be performed:
- Lookup the FACTORY_CLASS_DECLARATION attribute
- - If the newly created class is named 'FooBarFactory', look for a FooBar
- class in its module
- If an inherited associated class was provided, use it.
Args:
@@ -151,78 +101,112 @@ class FactoryMetaClass(BaseFactoryMetaClass):
AssociatedClassError: If we were unable to associate this factory
to a class.
"""
- own_associated_class = None
- used_auto_discovery = False
-
if FACTORY_CLASS_DECLARATION in attrs:
return attrs[FACTORY_CLASS_DECLARATION]
- # No specific associated calss was given, and one was defined for our
+ # No specific associated class was given, and one was defined for our
# parent, use it.
if inherited is not None:
return inherited
- if '__module__' in attrs:
- factory_module = sys.modules[attrs['__module__']]
- if class_name.endswith('Factory'):
- # Try a module lookup
- used_auto_discovery = True
- associated_name = class_name[:-len('Factory')]
- if associated_name and hasattr(factory_module, associated_name):
- warnings.warn(
- "Auto-discovery of associated class is deprecated, and "
- "will be removed in the future. Please set '%s = %s' "
- "in the %s class definition." % (
- FACTORY_CLASS_DECLARATION,
- associated_name,
- class_name,
- ), PendingDeprecationWarning)
-
- return getattr(factory_module, associated_name)
-
- # Unable to guess a good option; return the inherited class.
- # Unable to find an associated class; fail.
- if used_auto_discovery:
- raise Factory.AssociatedClassError(
- FactoryMetaClass.ERROR_MESSAGE_AUTODISCOVERY.format(
- FACTORY_CLASS_DECLARATION,
- associated_name,
- class_name,
- factory_module,))
- else:
- raise Factory.AssociatedClassError(
- FactoryMetaClass.ERROR_MESSAGE.format(
- FACTORY_CLASS_DECLARATION))
+ raise AssociatedClassError(
+ "Could not determine the class associated with %s. "
+ "Use the FACTORY_FOR attribute to specify an associated class." %
+ class_name)
+
+ @classmethod
+ def _extract_declarations(mcs, bases, attributes):
+ """Extract declarations from a class definition.
- def __new__(cls, class_name, bases, attrs):
- """Determine the associated class based on the factory class name. Record the associated class
- for construction of an associated class instance at a later time."""
+ Args:
+ bases (class list): parent Factory subclasses
+ attributes (dict): attributes declared in the class definition
+
+ Returns:
+ dict: the original attributes, where declarations have been moved to
+ _declarations and post-generation declarations to
+ _postgen_declarations.
+ """
+ declarations = containers.DeclarationDict()
+ postgen_declarations = containers.PostGenerationDeclarationDict()
+
+ # Add parent declarations in reverse order.
+ for base in reversed(bases):
+ # Import parent PostGenerationDeclaration
+ postgen_declarations.update_with_public(
+ getattr(base, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS, {}))
+ # Import all 'public' attributes (avoid those starting with _)
+ declarations.update_with_public(
+ getattr(base, CLASS_ATTRIBUTE_DECLARATIONS, {}))
+
+ # Import attributes from the class definition
+ attributes = postgen_declarations.update_with_public(attributes)
+ # Store protected/private attributes in 'non_factory_attrs'.
+ attributes = declarations.update_with_public(attributes)
+
+ # Store the DeclarationDict in the attributes of the newly created class
+ attributes[CLASS_ATTRIBUTE_DECLARATIONS] = declarations
+ attributes[CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS] = postgen_declarations
+
+ return attributes
+
+ def __new__(mcs, class_name, bases, attrs, extra_attrs=None):
+ """Record attributes as a pattern for later instance construction.
+
+ This is called when a new Factory subclass is defined; it will collect
+ attribute declaration from the class definition.
+
+ Args:
+ class_name (str): the name of the class being created
+ 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
+ """
parent_factories = get_factory_bases(bases)
- if not parent_factories or attrs.get('ABSTRACT_FACTORY', False):
- # If this isn't a subclass of Factory, don't do anything special.
- return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs)
+ if not parent_factories:
+ return super(FactoryMetaClass, mcs).__new__(
+ mcs, class_name, bases, attrs)
- base = parent_factories[0]
+ is_abstract = attrs.pop('ABSTRACT_FACTORY', False)
+ extra_attrs = {}
- inherited_associated_class = getattr(base,
- CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None)
- associated_class = cls._discover_associated_class(class_name, attrs,
- inherited_associated_class)
+ if not is_abstract:
- # Remove the FACTORY_CLASS_DECLARATION attribute from attrs, if present.
- attrs.pop(FACTORY_CLASS_DECLARATION, None)
+ base = parent_factories[0]
- # 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:
- attrs['_base_factory'] = base
+ inherited_associated_class = getattr(base,
+ CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None)
+ associated_class = mcs._discover_associated_class(class_name, attrs,
+ inherited_associated_class)
- # 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}
+ # 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:
+ attrs['_base_factory'] = base
- return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs, extra_attrs=extra_attrs)
+ # 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}
+
+ # 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)
+
+ 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__)
# Factory base classes
@@ -230,15 +214,13 @@ class FactoryMetaClass(BaseFactoryMetaClass):
class BaseFactory(object):
"""Factory base support for sequences, attributes and stubs."""
- class UnknownStrategy(RuntimeError):
- pass
-
- class UnsupportedStrategy(RuntimeError):
- pass
+ # Backwards compatibility
+ UnknownStrategy = UnknownStrategy
+ UnsupportedStrategy = UnsupportedStrategy
def __new__(cls, *args, **kwargs):
"""Would be called if trying to instantiate the class."""
- raise RuntimeError('You cannot instantiate BaseFactory')
+ raise FactoryError('You cannot instantiate BaseFactory')
# ID to use for the next 'declarations.Sequence' attribute.
_next_sequence = None
@@ -248,6 +230,15 @@ class BaseFactory(object):
# class.
_base_factory = None
+ # Holds the target class, once resolved.
+ _associated_class = None
+
+ # List of arguments that should be passed as *args instead of **kwargs
+ FACTORY_ARG_PARAMETERS = ()
+
+ # List of attributes that should not be passed to the underlying class
+ FACTORY_HIDDEN_ARGS = ()
+
@classmethod
def _setup_next_sequence(cls):
"""Set up an initial sequence value for Sequence attributes.
@@ -291,7 +282,13 @@ class BaseFactory(object):
applicable; the current list of computed attributes is available
to the currently processed object.
"""
- return containers.AttributeBuilder(cls, extra).build(create)
+ force_sequence = None
+ if extra:
+ force_sequence = extra.pop('__sequence', None)
+ return containers.AttributeBuilder(cls, extra).build(
+ create=create,
+ force_sequence=force_sequence,
+ )
@classmethod
def declarations(cls, extra_defs=None):
@@ -304,9 +301,108 @@ class BaseFactory(object):
return getattr(cls, CLASS_ATTRIBUTE_DECLARATIONS).copy(extra_defs)
@classmethod
+ def _adjust_kwargs(cls, **kwargs):
+ """Extension point for custom kwargs adjustment."""
+ return kwargs
+
+ @classmethod
+ def _prepare(cls, create, **kwargs):
+ """Prepare an object for this factory.
+
+ Args:
+ create: bool, whether to create or to build the object
+ **kwargs: arguments to pass to the creation function
+ """
+ target_class = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS)
+ kwargs = cls._adjust_kwargs(**kwargs)
+
+ # Remove 'hidden' arguments.
+ for arg in cls.FACTORY_HIDDEN_ARGS:
+ del kwargs[arg]
+
+ # Extract *args from **kwargs
+ args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS)
+
+ if create:
+ return cls._create(target_class, *args, **kwargs)
+ else:
+ return cls._build(target_class, *args, **kwargs)
+
+ @classmethod
+ def _generate(cls, create, attrs):
+ """generate the object.
+
+ Args:
+ create (bool): whether to 'build' or 'create' the object
+ attrs (dict): attributes to use for generating the object
+ """
+ # Extract declarations used for post-generation
+ postgen_declarations = getattr(cls,
+ CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS)
+ postgen_attributes = {}
+ for name, decl in sorted(postgen_declarations.items()):
+ postgen_attributes[name] = decl.extract(name, attrs)
+
+ # Generate the object
+ obj = cls._prepare(create, **attrs)
+
+ # Handle post-generation attributes
+ results = {}
+ for name, decl in sorted(postgen_declarations.items()):
+ extracted, extracted_kwargs = postgen_attributes[name]
+ results[name] = decl.call(obj, create, extracted,
+ **extracted_kwargs)
+
+ cls._after_postgeneration(obj, create, results)
+
+ return obj
+
+ @classmethod
+ def _after_postgeneration(cls, obj, create, results=None):
+ """Hook called after post-generation declarations have been handled.
+
+ Args:
+ obj (object): the generated object
+ create (bool): whether the strategy was 'build' or 'create'
+ results (dict or None): result of post-generation declarations
+ """
+ pass
+
+ @classmethod
+ def _build(cls, target_class, *args, **kwargs):
+ """Actually build an instance of the target_class.
+
+ Customization point, will be called once the full set of args and kwargs
+ has been computed.
+
+ Args:
+ target_class (type): the class for which an instance should be
+ built
+ args (tuple): arguments to use when building the class
+ kwargs (dict): keyword arguments to use when building the class
+ """
+ return target_class(*args, **kwargs)
+
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ """Actually create an instance of the target_class.
+
+ Customization point, will be called once the full set of args and kwargs
+ has been computed.
+
+ Args:
+ target_class (type): the class for which an instance should be
+ created
+ args (tuple): arguments to use when creating the class
+ kwargs (dict): keyword arguments to use when creating the class
+ """
+ return target_class(*args, **kwargs)
+
+ @classmethod
def build(cls, **kwargs):
"""Build an instance of the associated class, with overriden attrs."""
- raise cls.UnsupportedStrategy()
+ attrs = cls.attributes(create=False, extra=kwargs)
+ return cls._generate(False, attrs)
@classmethod
def build_batch(cls, size, **kwargs):
@@ -314,16 +410,17 @@ class BaseFactory(object):
Args:
size (int): the number of instances to build
-
+
Returns:
object list: the built instances
"""
- return [cls.build(**kwargs) for _ in xrange(size)]
+ return [cls.build(**kwargs) for _ in range(size)]
@classmethod
def create(cls, **kwargs):
"""Create an instance of the associated class, with overriden attrs."""
- raise cls.UnsupportedStrategy()
+ attrs = cls.attributes(create=True, extra=kwargs)
+ return cls._generate(True, attrs)
@classmethod
def create_batch(cls, size, **kwargs):
@@ -331,11 +428,11 @@ class BaseFactory(object):
Args:
size (int): the number of instances to create
-
+
Returns:
object list: the created instances
"""
- return [cls.create(**kwargs) for _ in xrange(size)]
+ return [cls.create(**kwargs) for _ in range(size)]
@classmethod
def stub(cls, **kwargs):
@@ -345,7 +442,7 @@ class BaseFactory(object):
factory's declarations or in the extra kwargs.
"""
stub_object = containers.StubObject()
- for name, value in cls.attributes(create=False, extra=kwargs).iteritems():
+ for name, value in cls.attributes(create=False, extra=kwargs).items():
setattr(stub_object, name, value)
return stub_object
@@ -355,11 +452,11 @@ class BaseFactory(object):
Args:
size (int): the number of instances to stub
-
+
Returns:
object list: the stubbed instances
"""
- return [cls.stub(**kwargs) for _ in xrange(size)]
+ return [cls.stub(**kwargs) for _ in range(size)]
@classmethod
def generate(cls, strategy, **kwargs):
@@ -428,189 +525,160 @@ class BaseFactory(object):
return cls.generate_batch(strategy, size, **kwargs)
-class StubFactory(BaseFactory):
- __metaclass__ = BaseFactoryMetaClass
-
- default_strategy = STUB_STRATEGY
-
-
-class Factory(BaseFactory):
- """Factory base with build and create support.
+Factory = FactoryMetaClass('Factory', (BaseFactory,), {
+ 'ABSTRACT_FACTORY': True,
+ 'FACTORY_STRATEGY': CREATE_STRATEGY,
+ '__doc__': """Factory base with build and create support.
This class has the ability to support multiple ORMs by using custom creation
functions.
- """
- __metaclass__ = FactoryMetaClass
+ """,
+ })
- default_strategy = CREATE_STRATEGY
- class AssociatedClassError(RuntimeError):
- pass
-
- # Customizing 'create' strategy, using a tuple to keep the creation function
- # from turning it into an instance method.
- _creation_function = (DJANGO_CREATION,)
+# Backwards compatibility
+Factory.AssociatedClassError = AssociatedClassError # pylint: disable=W0201
- def __str__(self):
- return '<%s for %s>' % (self.__class__.__name__,
- getattr(self, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__)
- @classmethod
- def set_creation_function(cls, creation_function):
- """Set the creation function for this class.
+class StubFactory(Factory):
- Args:
- creation_function (function): the new creation function. That
- function should take one non-keyword argument, the 'class' for
- which an instance will be created. The value of the various
- fields are passed as keyword arguments.
- """
- cls._creation_function = (creation_function,)
-
- @classmethod
- def get_creation_function(cls):
- """Retrieve the creation function for this class.
-
- Returns:
- function: A function that takes one parameter, the class for which
- an instance will be created, and keyword arguments for the value
- of the fields of the instance.
- """
- return cls._creation_function[0]
-
- # Customizing 'build' strategy, using a tuple to keep the creation function
- # from turning it into an instance method.
- _building_function = (NAIVE_BUILD,)
-
- @classmethod
- def set_building_function(cls, building_function):
- """Set the building function for this class.
-
- Args:
- building_function (function): the new building function. That
- function should take one non-keyword argument, the 'class' for
- which an instance will be built. The value of the various
- fields are passed as keyword arguments.
- """
- cls._building_function = (building_function,)
-
- @classmethod
- def get_building_function(cls):
- """Retrieve the building function for this class.
-
- Returns:
- function: A function that takes one parameter, the class for which
- an instance will be created, and keyword arguments for the value
- of the fields of the instance.
- """
- return cls._building_function[0]
-
- @classmethod
- def _prepare(cls, create, **kwargs):
- """Prepare an object for this factory.
-
- Args:
- create: bool, whether to create or to build the object
- **kwargs: arguments to pass to the creation function
- """
- if create:
- return cls.get_creation_function()(getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS), **kwargs)
- else:
- return cls.get_building_function()(getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS), **kwargs)
-
- @classmethod
- def _build(cls, **kwargs):
- return cls._prepare(create=False, **kwargs)
-
- @classmethod
- def _create(cls, **kwargs):
- return cls._prepare(create=True, **kwargs)
+ FACTORY_STRATEGY = STUB_STRATEGY
+ FACTORY_FOR = containers.StubObject
@classmethod
def build(cls, **kwargs):
- return cls._build(**cls.attributes(create=False, extra=kwargs))
+ raise UnsupportedStrategy()
@classmethod
def create(cls, **kwargs):
- return cls._create(**cls.attributes(create=True, extra=kwargs))
+ raise UnsupportedStrategy()
class DjangoModelFactory(Factory):
"""Factory for Django models.
- This makes sure that the 'sequence' field of created objects is an unused id.
+ This makes sure that the 'sequence' field of created objects is a new id.
Possible improvement: define a new 'attribute' type, AutoField, which would
handle those for non-numerical primary keys.
"""
ABSTRACT_FACTORY = True
+ FACTORY_DJANGO_GET_OR_CREATE = ()
+
+ @classmethod
+ def _get_manager(cls, target_class):
+ try:
+ return target_class._default_manager # pylint: disable=W0212
+ except AttributeError:
+ return target_class.objects
@classmethod
def _setup_next_sequence(cls):
- """Compute the next available ID, based on the 'id' database field."""
+ """Compute the next available PK, based on the 'pk' database field."""
+
+ model = cls._associated_class # pylint: disable=E1101
+ manager = cls._get_manager(model)
+
try:
- return 1 + cls._associated_class._default_manager.values_list('id', flat=True
- ).order_by('-id')[0]
+ return 1 + manager.values_list('pk', flat=True
+ ).order_by('-pk')[0]
except IndexError:
return 1
+ @classmethod
+ def _get_or_create(cls, target_class, *args, **kwargs):
+ """Create an instance of the model through objects.get_or_create."""
+ manager = cls._get_manager(target_class)
+
+ assert 'defaults' not in cls.FACTORY_DJANGO_GET_OR_CREATE, (
+ "'defaults' is a reserved keyword for get_or_create "
+ "(in %s.FACTORY_DJANGO_GET_OR_CREATE=%r)"
+ % (cls, cls.FACTORY_DJANGO_GET_OR_CREATE))
+
+ key_fields = {}
+ for field in cls.FACTORY_DJANGO_GET_OR_CREATE:
+ key_fields[field] = kwargs.pop(field)
+ key_fields['defaults'] = kwargs
+
+ obj, _created = manager.get_or_create(*args, **key_fields)
+ return obj
+
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ """Create an instance of the model, and save it to the database."""
+ manager = cls._get_manager(target_class)
+
+ if cls.FACTORY_DJANGO_GET_OR_CREATE:
+ return cls._get_or_create(target_class, *args, **kwargs)
-def make_factory(klass, **kwargs):
- """Create a new, simple factory for the given class."""
- factory_name = '%sFactory' % klass.__name__
- kwargs[FACTORY_CLASS_DECLARATION] = klass
- factory_class = type(Factory).__new__(type(Factory), factory_name, (Factory,), kwargs)
- factory_class.__name__ = '%sFactory' % klass.__name__
- factory_class.__doc__ = 'Auto-generated factory for class %s' % klass
- return factory_class
+ return manager.create(*args, **kwargs)
+ @classmethod
+ def _after_postgeneration(cls, obj, create, results=None):
+ """Save again the instance if creating and at least one hook ran."""
+ if create and results:
+ # Some post-generation hooks ran, and may have modified us.
+ obj.save()
-def build(klass, **kwargs):
- """Create a factory for the given class, and build an instance."""
- return make_factory(klass, **kwargs).build()
+class MogoFactory(Factory):
+ """Factory for mogo objects."""
+ ABSTRACT_FACTORY = True
-def build_batch(klass, size, **kwargs):
- """Create a factory for the given class, and build a batch of instances."""
- return make_factory(klass, **kwargs).build_batch(size)
+ @classmethod
+ def _build(cls, target_class, *args, **kwargs):
+ return target_class.new(*args, **kwargs)
-def create(klass, **kwargs):
- """Create a factory for the given class, and create an instance."""
- return make_factory(klass, **kwargs).create()
+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)
-def create_batch(klass, size, **kwargs):
- """Create a factory for the given class, and create a batch of instances."""
- return make_factory(klass, **kwargs).create_batch(size)
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ return cls._build(target_class, *args, **kwargs)
-def stub(klass, **kwargs):
- """Create a factory for the given class, and stub an instance."""
- return make_factory(klass, **kwargs).stub()
+class DictFactory(BaseDictFactory):
+ FACTORY_FOR = dict
-def stub_batch(klass, size, **kwargs):
- """Create a factory for the given class, and stub a batch of instances."""
- return make_factory(klass, **kwargs).stub_batch(size)
+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)
-def generate(klass, strategy, **kwargs):
- """Create a factory for the given class, and generate an instance."""
- return make_factory(klass, **kwargs).generate(strategy)
+ 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)
-def generate_batch(klass, strategy, size, **kwargs):
- """Create a factory for the given class, and generate instances."""
- return make_factory(klass, **kwargs).generate_batch(strategy, size)
+class ListFactory(BaseListFactory):
+ FACTORY_FOR = list
-def simple_generate(klass, create, **kwargs):
- """Create a factory for the given class, and simple_generate an instance."""
- return make_factory(klass, **kwargs).simple_generate(create)
+def use_strategy(new_strategy):
+ """Force the use of a different strategy.
-def simple_generate_batch(klass, create, size, **kwargs):
- """Create a factory for the given class, and simple_generate instances."""
- return make_factory(klass, **kwargs).simple_generate_batch(create, size)
+ This is an alternative to setting default_strategy in the class definition.
+ """
+ def wrapped_class(klass):
+ klass.FACTORY_STRATEGY = new_strategy
+ return klass
+ return wrapped_class