summaryrefslogtreecommitdiff
path: root/factory
diff options
context:
space:
mode:
Diffstat (limited to 'factory')
-rw-r--r--factory/__init__.py56
-rw-r--r--factory/base.py636
-rw-r--r--factory/compat.py35
-rw-r--r--factory/containers.py114
-rw-r--r--factory/declarations.py344
-rw-r--r--factory/fuzzy.py86
-rw-r--r--factory/helpers.py123
-rw-r--r--factory/utils.py96
8 files changed, 1063 insertions, 427 deletions
diff --git a/factory/__init__.py b/factory/__init__.py
index 1d4408f..e1138fa 100644
--- a/factory/__init__.py
+++ b/factory/__init__.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,51 +20,59 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-__version__ = '1.1.2' # Remember to change in setup.py as well!
-__author__ = 'Raphaël Barrois <raphael.barrois@polytechnique.org>'
+__version__ = '2.0.2'
+__author__ = 'Raphaël Barrois <raphael.barrois+fboy@polytechnique.org>'
-from base import (
+from .base import (
Factory,
+ BaseDictFactory,
+ DictFactory,
+ BaseListFactory,
+ ListFactory,
+ MogoFactory,
StubFactory,
DjangoModelFactory,
- build,
- create,
- stub,
- generate,
- simple_generate,
- make_factory,
-
- build_batch,
- create_batch,
- stub_batch,
- generate_batch,
- simple_generate_batch,
-
BUILD_STRATEGY,
CREATE_STRATEGY,
STUB_STRATEGY,
-
- DJANGO_CREATION,
- NAIVE_BUILD,
- MOGO_BUILD,
+ use_strategy,
)
-from declarations import (
+from .declarations import (
LazyAttribute,
Iterator,
- InfiniteIterator,
Sequence,
LazyAttributeSequence,
SelfAttribute,
ContainerAttribute,
SubFactory,
+ Dict,
+ List,
+ PostGeneration,
+ PostGenerationMethodCall,
+ RelatedFactory,
+)
+
+from .helpers import (
+ build,
+ create,
+ stub,
+ generate,
+ simple_generate,
+ make_factory,
+
+ build_batch,
+ create_batch,
+ stub_batch,
+ generate_batch,
+ simple_generate_batch,
lazy_attribute,
iterator,
- infinite_iterator,
sequence,
lazy_attribute_sequence,
container_attribute,
+ post_generation,
)
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
diff --git a/factory/compat.py b/factory/compat.py
new file mode 100644
index 0000000..84f31b7
--- /dev/null
+++ b/factory/compat.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Mark Sandstrom
+# 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+
+"""Compatibility tools"""
+
+import sys
+
+PY2 = (sys.version_info[0] == 2)
+
+if PY2:
+ def is_string(obj):
+ return isinstance(obj, (str, unicode))
+else:
+ def is_string(obj):
+ return isinstance(obj, str)
diff --git a/factory/containers.py b/factory/containers.py
index 2f92f62..ee2ad82 100644
--- a/factory/containers.py
+++ b/factory/containers.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
@@ -21,12 +21,8 @@
# THE SOFTWARE.
-from factory import declarations
-
-
-#: String for splitting an attribute name into a
-#: (subfactory_name, subfactory_field) tuple.
-ATTR_SPLITTER = '__'
+from . import declarations
+from . import utils
class CyclicDefinitionError(Exception):
@@ -66,7 +62,7 @@ class LazyStub(object):
def __str__(self):
return '<LazyStub for %s with %s>' % (
- self.__target_class.__name__, self.__attrs.keys())
+ self.__target_class.__name__, list(self.__attrs.keys()))
def __fill__(self):
"""Fill this LazyStub, computing values of all defined attributes.
@@ -128,7 +124,7 @@ class DeclarationDict(dict):
return False
elif isinstance(value, declarations.OrderedDeclaration):
return True
- return (not name.startswith("_"))
+ return (not name.startswith("_") and not name.startswith("FACTORY_"))
def update_with_public(self, d):
"""Updates the DeclarationDict from a class definition dict.
@@ -140,7 +136,7 @@ class DeclarationDict(dict):
Returns a dict containing all remaining elements.
"""
remaining = {}
- for k, v in d.iteritems():
+ for k, v in d.items():
if self.is_declaration(k, v):
self[k] = v
else:
@@ -153,45 +149,29 @@ class DeclarationDict(dict):
Args:
extra (dict): additional attributes to include in the copy.
"""
- new = DeclarationDict()
+ new = self.__class__()
new.update(self)
if extra:
new.update(extra)
return new
+class PostGenerationDeclarationDict(DeclarationDict):
+ """Alternate DeclarationDict for PostGenerationDeclaration."""
+
+ def is_declaration(self, name, value):
+ """Captures instances of PostGenerationDeclaration."""
+ return isinstance(value, declarations.PostGenerationDeclaration)
+
+
class LazyValue(object):
"""Some kind of "lazy evaluating" object."""
- def evaluate(self, obj, containers=()):
+ def evaluate(self, obj, containers=()): # pragma: no cover
"""Compute the value, using the given object."""
raise NotImplementedError("This is an abstract method.")
-class SubFactoryWrapper(LazyValue):
- """Lazy wrapper around a SubFactory.
-
- Attributes:
- subfactory (declarations.SubFactory): the SubFactory being wrapped
- subfields (DeclarationDict): Default values to override when evaluating
- the SubFactory
- create (bool): whether to 'create' or 'build' the SubFactory.
- """
-
- def __init__(self, subfactory, subfields, create, *args, **kwargs):
- super(SubFactoryWrapper, self).__init__(*args, **kwargs)
- self.subfactory = subfactory
- self.subfields = subfields
- self.create = create
-
- def evaluate(self, obj, containers=()):
- expanded_containers = (obj,)
- if containers:
- expanded_containers += tuple(containers)
- return self.subfactory.evaluate(self.create, self.subfields,
- expanded_containers)
-
-
class OrderedDeclarationWrapper(LazyValue):
"""Lazy wrapper around an OrderedDeclaration.
@@ -202,10 +182,12 @@ class OrderedDeclarationWrapper(LazyValue):
declaration
"""
- def __init__(self, declaration, sequence, *args, **kwargs):
- super(OrderedDeclarationWrapper, self).__init__(*args, **kwargs)
+ def __init__(self, declaration, sequence, create, extra=None, **kwargs):
+ super(OrderedDeclarationWrapper, self).__init__(**kwargs)
self.declaration = declaration
self.sequence = sequence
+ self.create = create
+ self.extra = extra
def evaluate(self, obj, containers=()):
"""Lazily evaluate the attached OrderedDeclaration.
@@ -215,7 +197,14 @@ class OrderedDeclarationWrapper(LazyValue):
containers (object list): the chain of containers of the object
being built, its immediate holder being first.
"""
- return self.declaration.evaluate(self.sequence, obj, containers)
+ return self.declaration.evaluate(self.sequence, obj,
+ create=self.create,
+ extra=self.extra,
+ containers=containers,
+ )
+
+ def __repr__(self):
+ return '<%s for %r>' % (self.__class__.__name__, self.declaration)
class AttributeBuilder(object):
@@ -236,38 +225,43 @@ class AttributeBuilder(object):
extra = {}
self.factory = factory
- self._containers = extra.pop('__containers', None)
+ self._containers = extra.pop('__containers', ())
self._attrs = factory.declarations(extra)
- self._subfields = self._extract_subfields()
-
- def _extract_subfields(self):
- """Extract the subfields from the declarations list."""
- sub_fields = {}
- for key in list(self._attrs):
- if ATTR_SPLITTER in key:
- # Trying to define a default of a subfactory
- cls_name, attr_name = key.split(ATTR_SPLITTER, 1)
- if cls_name in self._attrs:
- sub_fields.setdefault(cls_name, {})[attr_name] = self._attrs.pop(key)
- return sub_fields
-
- def build(self, create):
+
+ attrs_with_subfields = [
+ k for k, v in self._attrs.items()
+ if self.has_subfields(v)]
+
+ self._subfields = utils.multi_extract_dict(
+ attrs_with_subfields, self._attrs)
+
+ def has_subfields(self, value):
+ return isinstance(value, declarations.ParameteredAttribute)
+
+ def build(self, create, force_sequence=None):
"""Build a dictionary of attributes.
Args:
create (bool): whether to 'build' or 'create' the subfactories.
+ force_sequence (int or None): if set to an int, use this value for
+ the sequence counter; don't advance the related counter.
"""
# Setup factory sequence.
- self.factory.sequence = self.factory._generate_next_sequence()
+ if force_sequence is None:
+ sequence = self.factory._generate_next_sequence()
+ else:
+ sequence = force_sequence
# Parse attribute declarations, wrapping SubFactory and
# OrderedDeclaration.
wrapped_attrs = {}
- for k, v in self._attrs.iteritems():
- if isinstance(v, declarations.SubFactory):
- v = SubFactoryWrapper(v, self._subfields.get(k, {}), create)
- elif isinstance(v, declarations.OrderedDeclaration):
- v = OrderedDeclarationWrapper(v, self.factory.sequence)
+ for k, v in self._attrs.items():
+ if isinstance(v, declarations.OrderedDeclaration):
+ v = OrderedDeclarationWrapper(v,
+ sequence=sequence,
+ create=create,
+ extra=self._subfields.get(k, {}),
+ )
wrapped_attrs[k] = v
stub = LazyStub(wrapped_attrs, containers=self._containers,
diff --git a/factory/declarations.py b/factory/declarations.py
index 41d99a3..974b4ac 100644
--- a/factory/declarations.py
+++ b/factory/declarations.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
@@ -23,6 +23,9 @@
import itertools
+from . import compat
+from . import utils
+
class OrderedDeclaration(object):
"""A factory declaration.
@@ -32,7 +35,7 @@ class OrderedDeclaration(object):
in the same factory.
"""
- def evaluate(self, sequence, obj, containers=()):
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
"""Evaluate this declaration.
Args:
@@ -42,6 +45,10 @@ class OrderedDeclaration(object):
attributes
containers (list of containers.LazyStub): The chain of SubFactory
which led to building this object.
+ create (bool): whether the target class should be 'built' or
+ 'created'
+ extra (DeclarationDict or None): extracted key/value extracted from
+ the attribute prefix
"""
raise NotImplementedError('This is an abstract method')
@@ -58,7 +65,7 @@ class LazyAttribute(OrderedDeclaration):
super(LazyAttribute, self).__init__(*args, **kwargs)
self.function = function
- def evaluate(self, sequence, obj, containers=()):
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
return self.function(obj)
@@ -98,7 +105,11 @@ def deepgetattr(obj, name, default=_UNSPECIFIED):
class SelfAttribute(OrderedDeclaration):
"""Specific OrderedDeclaration copying values from other fields.
+ If the field name starts with two dots or more, the lookup will be anchored
+ in the related 'parent'.
+
Attributes:
+ depth (int): the number of steps to go up in the containers chain
attribute_name (str): the name of the attribute to copy.
default (object): the default value to use if the attribute doesn't
exist.
@@ -106,11 +117,27 @@ class SelfAttribute(OrderedDeclaration):
def __init__(self, attribute_name, default=_UNSPECIFIED, *args, **kwargs):
super(SelfAttribute, self).__init__(*args, **kwargs)
+ depth = len(attribute_name) - len(attribute_name.lstrip('.'))
+ attribute_name = attribute_name[depth:]
+
+ self.depth = depth
self.attribute_name = attribute_name
self.default = default
- def evaluate(self, sequence, obj, containers=()):
- return deepgetattr(obj, self.attribute_name, self.default)
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
+ if self.depth > 1:
+ # Fetching from a parent
+ target = containers[self.depth - 2]
+ else:
+ target = obj
+ return deepgetattr(target, self.attribute_name, self.default)
+
+ def __repr__(self):
+ return '<%s(%r, default=%r)>' % (
+ self.__class__.__name__,
+ self.attribute_name,
+ self.default,
+ )
class Iterator(OrderedDeclaration):
@@ -120,25 +147,23 @@ class Iterator(OrderedDeclaration):
Attributes:
iterator (iterable): the iterator whose value should be used.
+ getter (callable or None): a function to parse returned values
"""
- def __init__(self, iterator):
+ def __init__(self, iterator, cycle=True, getter=None):
super(Iterator, self).__init__()
- self.iterator = iter(iterator)
+ self.getter = getter
- def evaluate(self, sequence, obj, containers=()):
- return self.iterator.next()
-
-
-class InfiniteIterator(Iterator):
- """Same as Iterator, but make the iterator infinite by cycling at the end.
-
- Attributes:
- iterator (iterable): the iterator, once made infinite.
- """
+ if cycle:
+ self.iterator = itertools.cycle(iterator)
+ else:
+ self.iterator = iter(iterator)
- def __init__(self, iterator):
- return super(InfiniteIterator, self).__init__(itertools.cycle(iterator))
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
+ value = next(self.iterator)
+ if self.getter is None:
+ return value
+ return self.getter(value)
class Sequence(OrderedDeclaration):
@@ -152,12 +177,12 @@ class Sequence(OrderedDeclaration):
type (function): A function converting an integer into the expected kind
of counter for the 'function' attribute.
"""
- def __init__(self, function, type=str):
+ def __init__(self, function, type=int): # pylint: disable=W0622
super(Sequence, self).__init__()
self.function = function
self.type = type
- def evaluate(self, sequence, obj, containers=()):
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
return self.function(self.type(sequence))
@@ -170,7 +195,7 @@ class LazyAttributeSequence(Sequence):
type (function): A function converting an integer into the expected kind
of counter for the 'function' attribute.
"""
- def evaluate(self, sequence, obj, containers=()):
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
return self.function(obj, self.type(sequence))
@@ -188,7 +213,7 @@ class ContainerAttribute(OrderedDeclaration):
self.function = function
self.strict = strict
- def evaluate(self, sequence, obj, containers=()):
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
"""Evaluate the current ContainerAttribute.
Args:
@@ -206,68 +231,269 @@ class ContainerAttribute(OrderedDeclaration):
return self.function(obj, containers)
-class SubFactory(OrderedDeclaration):
- """Base class for attributes based upon a sub-factory.
+class ParameteredAttribute(OrderedDeclaration):
+ """Base class for attributes expecting parameters.
Attributes:
- defaults (dict): Overrides to the defaults defined in the wrapped
- factory
- factory (base.Factory): the wrapped factory
+ defaults (dict): Default values for the paramters.
+ May be overridden by call-time parameters.
+
+ Class attributes:
+ CONTAINERS_FIELD (str): name of the field, if any, where container
+ information (e.g for SubFactory) should be stored. If empty,
+ containers data isn't merged into generate() parameters.
"""
- def __init__(self, factory, **kwargs):
- super(SubFactory, self).__init__()
+ CONTAINERS_FIELD = '__containers'
+
+ # Whether to add the current object to the stack of containers
+ EXTEND_CONTAINERS = False
+
+ def __init__(self, **kwargs):
+ super(ParameteredAttribute, self).__init__()
self.defaults = kwargs
- self.factory = factory
- def evaluate(self, create, extra, containers):
+ def _prepare_containers(self, obj, containers=()):
+ if self.EXTEND_CONTAINERS:
+ return (obj,) + tuple(containers)
+
+ return containers
+
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
"""Evaluate the current definition and fill its attributes.
Uses attributes definition in the following order:
- - attributes defined in the wrapped factory class
- - values defined when defining the SubFactory
- - additional values defined in attributes
+ - values defined when defining the ParameteredAttribute
+ - additional values defined when instantiating the containing factory
Args:
- create (bool): whether the subfactory should call 'build' or
- 'create'
+ create (bool): whether the parent factory is being 'built' or
+ 'created'
extra (containers.DeclarationDict): extra values that should
- override the wrapped factory's defaults
+ override the defaults
containers (list of LazyStub): List of LazyStub for the chain of
factories being evaluated, the calling stub being first.
"""
-
defaults = dict(self.defaults)
if extra:
defaults.update(extra)
- defaults['__containers'] = containers
+ if self.CONTAINERS_FIELD:
+ containers = self._prepare_containers(obj, containers)
+ defaults[self.CONTAINERS_FIELD] = containers
+
+ return self.generate(sequence, obj, create, defaults)
+
+ 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
+ overrides.
+
+ Returns:
+ Computed value for the current declaration.
+ """
+ raise NotImplementedError()
+
+
+class SubFactory(ParameteredAttribute):
+ """Base class for attributes based upon a sub-factory.
+
+ Attributes:
+ defaults (dict): Overrides to the defaults defined in the wrapped
+ factory
+ factory (base.Factory): the wrapped factory
+ """
- attrs = self.factory.attributes(create, defaults)
+ EXTEND_CONTAINERS = True
- if create:
- return self.factory.create(**attrs)
+ def __init__(self, factory, **kwargs):
+ super(SubFactory, self).__init__(**kwargs)
+ if isinstance(factory, type):
+ self.factory = factory
+ self.factory_module = self.factory_name = ''
else:
- return self.factory.build(**attrs)
+ # Must be a string
+ if not (compat.is_string(factory) and '.' in factory):
+ raise ValueError(
+ "The argument of a SubFactory must be either a class "
+ "or the fully qualified path to a Factory class; got "
+ "%r instead." % factory)
+ self.factory = None
+ self.factory_module, self.factory_name = factory.rsplit('.', 1)
+
+ def get_factory(self):
+ """Retrieve the wrapped factory.Factory subclass."""
+ if self.factory is None:
+ # Must be a module path
+ self.factory = utils.import_object(
+ self.factory_module, self.factory_name)
+ return self.factory
+
+ def generate(self, sequence, obj, create, params):
+ """Evaluate the current definition and fill its attributes.
+
+ Args:
+ create (bool): whether the subfactory should call 'build' or
+ 'create'
+ params (containers.DeclarationDict): extra values that should
+ override the wrapped factory's defaults
+ """
+ subfactory = self.get_factory()
+ return subfactory.simple_generate(create, **params)
-# Decorators... in case lambdas don't cut it
+class Dict(SubFactory):
+ """Fill a dict with usual declarations."""
-def lazy_attribute(func):
- return LazyAttribute(func)
+ def __init__(self, params, dict_factory='factory.DictFactory'):
+ super(Dict, self).__init__(dict_factory, **dict(params))
-def iterator(func):
- """Turn a generator function into an iterator attribute."""
- return Iterator(func())
+ def generate(self, sequence, obj, create, params):
+ dict_factory = self.get_factory()
+ return dict_factory.simple_generate(create,
+ __sequence=sequence,
+ **params)
-def infinite_iterator(func):
- """Turn a generator function into an infinite iterator attribute."""
- return InfiniteIterator(func())
-def sequence(func):
- return Sequence(func)
+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."""
+
+ def extract(self, name, attrs):
+ """Extract relevant attributes from a dict.
+
+ Args:
+ name (str): the name at which this PostGenerationDeclaration was
+ defined in the declarations
+ attrs (dict): the attribute dict from which values should be
+ extracted
+
+ Returns:
+ (object, dict): a tuple containing the attribute at 'name' (if
+ provided) and a dict of extracted attributes
+ """
+ extracted = attrs.pop(name, None)
+ kwargs = utils.extract_dict(name, attrs)
+ return extracted, kwargs
+
+ def call(self, obj, create, extracted=None, **kwargs): # pragma: no cover
+ """Call this hook; no return value is expected.
-def lazy_attribute_sequence(func):
- return LazyAttributeSequence(func)
+ Args:
+ obj (object): the newly generated object
+ create (bool): whether the object was 'built' or 'created'
+ extracted (object): the value given for <name> in the
+ object definition, or None if not provided.
+ kwargs (dict): declarations extracted from the object
+ definition for this hook
+ """
+ raise NotImplementedError()
+
+
+class PostGeneration(PostGenerationDeclaration):
+ """Calls a given function once the object has been generated."""
+ def __init__(self, function):
+ super(PostGeneration, self).__init__()
+ self.function = function
+
+ def call(self, obj, create, extracted=None, **kwargs):
+ return self.function(obj, create, extracted, **kwargs)
+
+
+class RelatedFactory(PostGenerationDeclaration):
+ """Calls a factory once the object has been generated.
+
+ Attributes:
+ factory (Factory): the factory to call
+ defaults (dict): extra declarations for calling the related factory
+ name (str): the name to use to refer to the generated object when
+ calling the related factory
+ """
+
+ def __init__(self, factory, name='', **defaults):
+ super(RelatedFactory, self).__init__()
+ self.name = name
+ self.defaults = defaults
+
+ if isinstance(factory, type):
+ self.factory = factory
+ self.factory_module = self.factory_name = ''
+ else:
+ # Must be a string
+ if not (compat.is_string(factory) and '.' in factory):
+ raise ValueError(
+ "The argument of a SubFactory must be either a class "
+ "or the fully qualified path to a Factory class; got "
+ "%r instead." % factory)
+ self.factory = None
+ self.factory_module, self.factory_name = factory.rsplit('.', 1)
+
+ def get_factory(self):
+ """Retrieve the wrapped factory.Factory subclass."""
+ if self.factory is None:
+ # Must be a module path
+ self.factory = utils.import_object(
+ self.factory_module, self.factory_name)
+ return self.factory
+
+ def call(self, obj, create, extracted=None, **kwargs):
+ passed_kwargs = dict(self.defaults)
+ passed_kwargs.update(kwargs)
+ if self.name:
+ passed_kwargs[self.name] = obj
+
+ factory = self.get_factory()
+ factory.simple_generate(create, **passed_kwargs)
+
+
+class PostGenerationMethodCall(PostGenerationDeclaration):
+ """Calls a method of the generated object.
+
+ Attributes:
+ method_name (str): the method to call
+ method_args (list): arguments to pass to the method
+ method_kwargs (dict): keyword arguments to pass to the method
+
+ Example:
+ class UserFactory(factory.Factory):
+ ...
+ password = factory.PostGenerationMethodCall('set_pass', password='')
+ """
+ def __init__(self, method_name, *args, **kwargs):
+ super(PostGenerationMethodCall, self).__init__()
+ self.method_name = method_name
+ self.method_args = args
+ self.method_kwargs = kwargs
+
+ def call(self, obj, create, extracted=None, **kwargs):
+ if extracted is None:
+ passed_args = self.method_args
+
+ elif len(self.method_args) <= 1:
+ # Max one argument expected
+ passed_args = (extracted,)
+ else:
+ passed_args = tuple(extracted)
-def container_attribute(func):
- return ContainerAttribute(func, strict=False)
+ passed_kwargs = dict(self.method_kwargs)
+ passed_kwargs.update(kwargs)
+ method = getattr(obj, self.method_name)
+ method(*passed_args, **passed_kwargs)
diff --git a/factory/fuzzy.py b/factory/fuzzy.py
new file mode 100644
index 0000000..186b4a7
--- /dev/null
+++ b/factory/fuzzy.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Mark Sandstrom
+# 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+
+"""Additional declarations for "fuzzy" attribute definitions."""
+
+
+import random
+
+from . import declarations
+
+
+class BaseFuzzyAttribute(declarations.OrderedDeclaration):
+ """Base class for fuzzy attributes.
+
+ Custom fuzzers should override the `fuzz()` method.
+ """
+
+ def fuzz(self):
+ raise NotImplementedError()
+
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
+ return self.fuzz()
+
+
+class FuzzyAttribute(BaseFuzzyAttribute):
+ """Similar to LazyAttribute, but yields random values.
+
+ Attributes:
+ function (callable): function taking no parameters and returning a
+ random value.
+ """
+
+ def __init__(self, fuzzer, **kwargs):
+ super(FuzzyAttribute, self).__init__(**kwargs)
+ self.fuzzer = fuzzer
+
+ def fuzz(self):
+ return self.fuzzer()
+
+
+class FuzzyChoice(BaseFuzzyAttribute):
+ """Handles fuzzy choice of an attribute."""
+
+ def __init__(self, choices, **kwargs):
+ self.choices = list(choices)
+ super(FuzzyChoice, self).__init__(**kwargs)
+
+ def fuzz(self):
+ return random.choice(self.choices)
+
+
+class FuzzyInteger(BaseFuzzyAttribute):
+ """Random integer within a given range."""
+
+ def __init__(self, low, high=None, **kwargs):
+ if high is None:
+ high = low
+ low = 0
+
+ self.low = low
+ self.high = high
+
+ super(FuzzyInteger, self).__init__(**kwargs)
+
+ def fuzz(self):
+ return random.randint(self.low, self.high)
diff --git a/factory/helpers.py b/factory/helpers.py
new file mode 100644
index 0000000..8f0d161
--- /dev/null
+++ b/factory/helpers.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Mark Sandstrom
+# 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+
+"""Simple wrappers around Factory class definition."""
+
+
+from . import base
+from . import declarations
+
+
+def make_factory(klass, **kwargs):
+ """Create a new, simple factory for the given class."""
+ factory_name = '%sFactory' % klass.__name__
+ kwargs[base.FACTORY_CLASS_DECLARATION] = klass
+ base_class = kwargs.pop('FACTORY_CLASS', base.Factory)
+
+ factory_class = type(base.Factory).__new__(
+ type(base.Factory), factory_name, (base_class,), kwargs)
+ factory_class.__name__ = '%sFactory' % klass.__name__
+ factory_class.__doc__ = 'Auto-generated factory for class %s' % klass
+ return factory_class
+
+
+def build(klass, **kwargs):
+ """Create a factory for the given class, and build an instance."""
+ return make_factory(klass, **kwargs).build()
+
+
+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)
+
+
+def create(klass, **kwargs):
+ """Create a factory for the given class, and create an instance."""
+ return make_factory(klass, **kwargs).create()
+
+
+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)
+
+
+def stub(klass, **kwargs):
+ """Create a factory for the given class, and stub an instance."""
+ return make_factory(klass, **kwargs).stub()
+
+
+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)
+
+
+def generate(klass, strategy, **kwargs):
+ """Create a factory for the given class, and generate an instance."""
+ return make_factory(klass, **kwargs).generate(strategy)
+
+
+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)
+
+
+# We're reusing 'create' as a keyword.
+# pylint: disable=W0621
+
+
+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 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)
+
+
+# pylint: enable=W0621
+
+
+def lazy_attribute(func):
+ return declarations.LazyAttribute(func)
+
+
+def iterator(func):
+ """Turn a generator function into an iterator attribute."""
+ return declarations.Iterator(func())
+
+
+def sequence(func):
+ return declarations.Sequence(func)
+
+
+def lazy_attribute_sequence(func):
+ return declarations.LazyAttributeSequence(func)
+
+
+def container_attribute(func):
+ return declarations.ContainerAttribute(func, strict=False)
+
+
+def post_generation(fun):
+ return declarations.PostGeneration(fun)
diff --git a/factory/utils.py b/factory/utils.py
new file mode 100644
index 0000000..fb8cfef
--- /dev/null
+++ b/factory/utils.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Mark Sandstrom
+# 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+
+#: String for splitting an attribute name into a
+#: (subfactory_name, subfactory_field) tuple.
+ATTR_SPLITTER = '__'
+
+def extract_dict(prefix, kwargs, pop=True, exclude=()):
+ """Extracts all values beginning with a given prefix from a dict.
+
+ Can either 'pop' or 'get' them;
+
+ Args:
+ prefix (str): the prefix to use for lookups
+ kwargs (dict): the dict from which values should be extracted
+ pop (bool): whether to use pop (True) or get (False)
+ exclude (iterable): list of prefixed keys that shouldn't be extracted
+
+ Returns:
+ A new dict, containing values from kwargs and beginning with
+ prefix + ATTR_SPLITTER. That full prefix is removed from the keys
+ of the returned dict.
+ """
+ prefix = prefix + ATTR_SPLITTER
+ extracted = {}
+
+ for key in list(kwargs):
+ if key in exclude:
+ continue
+
+ if key.startswith(prefix):
+ new_key = key[len(prefix):]
+ if pop:
+ value = kwargs.pop(key)
+ else:
+ value = kwargs[key]
+ extracted[new_key] = value
+ return extracted
+
+
+def multi_extract_dict(prefixes, kwargs, pop=True, exclude=()):
+ """Extracts all values from a given list of prefixes.
+
+ Extraction will start with longer prefixes.
+
+ Args:
+ prefixes (str list): the prefixes to use for lookups
+ kwargs (dict): the dict from which values should be extracted
+ pop (bool): whether to use pop (True) or get (False)
+ exclude (iterable): list of prefixed keys that shouldn't be extracted
+
+ Returns:
+ dict(str => dict): a dict mapping each prefix to the dict of extracted
+ key/value.
+ """
+ results = {}
+ exclude = list(exclude)
+ for prefix in sorted(prefixes, key=lambda x: -len(x)):
+ extracted = extract_dict(prefix, kwargs, pop=pop, exclude=exclude)
+ results[prefix] = extracted
+ exclude.extend(
+ ['%s%s%s' % (prefix, ATTR_SPLITTER, key) for key in extracted])
+
+ return results
+
+
+def import_object(module_name, attribute_name):
+ """Import an object from its absolute path.
+
+ Example:
+ >>> import_object('datetime', 'datetime')
+ <type 'datetime.datetime'>
+ """
+ module = __import__(module_name, {}, {}, [attribute_name], 0)
+ return getattr(module, attribute_name)
+