summaryrefslogtreecommitdiff
path: root/factory
diff options
context:
space:
mode:
Diffstat (limited to 'factory')
-rw-r--r--factory/__init__.py19
-rw-r--r--factory/alchemy.py34
-rw-r--r--factory/base.py406
-rw-r--r--factory/compat.py10
-rw-r--r--factory/containers.py71
-rw-r--r--factory/declarations.py30
-rw-r--r--factory/django.py166
-rw-r--r--factory/faker.py101
-rw-r--r--factory/fuzzy.py53
-rw-r--r--factory/helpers.py7
-rw-r--r--factory/mogo.py13
-rw-r--r--factory/mongoengine.py14
-rw-r--r--factory/utils.py34
13 files changed, 572 insertions, 386 deletions
diff --git a/factory/__init__.py b/factory/__init__.py
index aa550e8..4a4a09f 100644
--- a/factory/__init__.py
+++ b/factory/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 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,7 +20,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-__version__ = '2.3.1'
+__version__ = '2.6.0'
__author__ = 'Raphaël Barrois <raphael.barrois+fboy@polytechnique.org>'
@@ -32,15 +32,15 @@ from .base import (
ListFactory,
StubFactory,
+ FactoryError,
+
BUILD_STRATEGY,
CREATE_STRATEGY,
STUB_STRATEGY,
use_strategy,
)
-# Backward compatibility; this should be removed soon.
-from .mogo import MogoFactory
-from .django import DjangoModelFactory
+from .faker import Faker
from .declarations import (
LazyAttribute,
@@ -81,3 +81,12 @@ from .helpers import (
post_generation,
)
+# Backward compatibility; this should be removed soon.
+from . import alchemy
+from . import django
+from . import mogo
+from . import mongoengine
+
+MogoFactory = mogo.MogoFactory
+DjangoModelFactory = django.DjangoModelFactory
+
diff --git a/factory/alchemy.py b/factory/alchemy.py
index cec15c9..a9aab23 100644
--- a/factory/alchemy.py
+++ b/factory/alchemy.py
@@ -19,33 +19,31 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from __future__ import unicode_literals
-from sqlalchemy.sql.functions import max
from . import base
+class SQLAlchemyOptions(base.FactoryOptions):
+ def _build_default_options(self):
+ return super(SQLAlchemyOptions, self)._build_default_options() + [
+ base.OptionDefault('sqlalchemy_session', None, inherit=True),
+ base.OptionDefault('force_flush', False, inherit=True),
+ ]
+
+
class SQLAlchemyModelFactory(base.Factory):
"""Factory for SQLAlchemy models. """
- ABSTRACT_FACTORY = True
- FACTORY_SESSION = None
-
- @classmethod
- def _setup_next_sequence(cls, *args, **kwargs):
- """Compute the next available PK, based on the 'pk' database field."""
- session = cls.FACTORY_SESSION
- model = cls.FACTORY_FOR
- pk = getattr(model, model.__mapper__.primary_key[0].name)
- max_pk = session.query(max(pk)).one()[0]
- if isinstance(max_pk, int):
- return max_pk + 1 if max_pk else 1
- else:
- return 1
+ _options_class = SQLAlchemyOptions
+ class Meta:
+ abstract = True
@classmethod
- def _create(cls, target_class, *args, **kwargs):
+ def _create(cls, model_class, *args, **kwargs):
"""Create an instance of the model, and save it to the database."""
- session = cls.FACTORY_SESSION
- obj = target_class(*args, **kwargs)
+ session = cls._meta.sqlalchemy_session
+ obj = model_class(*args, **kwargs)
session.add(obj)
+ if cls._meta.force_flush:
+ session.flush()
return obj
diff --git a/factory/base.py b/factory/base.py
index 3c6571c..0f2af59 100644
--- a/factory/base.py
+++ b/factory/base.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 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,7 @@
import logging
from . import containers
+from . import declarations
from . import utils
logger = logging.getLogger('factory.generate')
@@ -33,22 +34,13 @@ CREATE_STRATEGY = 'create'
STUB_STRATEGY = 'stub'
-# 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_ATTRIBUTE_IS_ABSTRACT = '_abstract_factory'
-
class FactoryError(Exception):
"""Any exception raised by factory_boy."""
class AssociatedClassError(FactoryError):
- """Exception for Factory subclasses lacking FACTORY_FOR."""
+ """Exception for Factory subclasses lacking Meta.model."""
class UnknownStrategy(FactoryError):
@@ -66,6 +58,14 @@ def get_factory_bases(bases):
return [b for b in bases if issubclass(b, BaseFactory)]
+def resolve_attribute(name, bases, default=None):
+ """Find the first definition of an attribute according to MRO order."""
+ for base in bases:
+ if hasattr(base, name):
+ return getattr(base, name)
+ return default
+
+
class FactoryMetaClass(type):
"""Factory metaclass for handling ordered declarations."""
@@ -75,142 +75,193 @@ class FactoryMetaClass(type):
Returns an instance of the associated class.
"""
- if cls.FACTORY_STRATEGY == BUILD_STRATEGY:
+ if cls._meta.strategy == BUILD_STRATEGY:
return cls.build(**kwargs)
- elif cls.FACTORY_STRATEGY == CREATE_STRATEGY:
+ elif cls._meta.strategy == CREATE_STRATEGY:
return cls.create(**kwargs)
- elif cls.FACTORY_STRATEGY == STUB_STRATEGY:
+ elif cls._meta.strategy == STUB_STRATEGY:
return cls.stub(**kwargs)
else:
- raise UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format(
- cls.FACTORY_STRATEGY))
+ raise UnknownStrategy('Unknown Meta.strategy: {0}'.format(
+ cls._meta.strategy))
- @classmethod
- def _discover_associated_class(mcs, class_name, attrs, inherited=None):
- """Try to find the class associated with this factory.
+ def __new__(mcs, class_name, bases, attrs):
+ """Record attributes as a pattern for later instance construction.
- In order, the following tests will be performed:
- - Lookup the FACTORY_CLASS_DECLARATION attribute
- - If an inherited associated class was provided, use it.
+ 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 factory class being created
- attrs (dict): the dict of attributes from the factory class
+ 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
- inherited (class): the optional associated class inherited from a
- parent factory
Returns:
- class: the class to associate with this factory
+ A new class
"""
- if FACTORY_CLASS_DECLARATION in attrs:
- return attrs[FACTORY_CLASS_DECLARATION]
+ parent_factories = get_factory_bases(bases)
+ if parent_factories:
+ base_factory = parent_factories[0]
+ else:
+ base_factory = None
- # No specific associated class was given, and one was defined for our
- # parent, use it.
- if inherited is not None:
- return inherited
+ attrs_meta = attrs.pop('Meta', None)
- # Nothing found, return None.
- return None
+ base_meta = resolve_attribute('_meta', bases)
+ options_class = resolve_attribute('_options_class', bases, FactoryOptions)
- @classmethod
- def _extract_declarations(mcs, bases, attributes):
- """Extract declarations from a class definition.
+ meta = options_class()
+ attrs['_meta'] = meta
- Args:
- bases (class list): parent Factory subclasses
- attributes (dict): attributes declared in the class definition
+ new_class = super(FactoryMetaClass, mcs).__new__(
+ mcs, class_name, bases, attrs)
- 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()
+ meta.contribute_to_class(new_class,
+ meta=attrs_meta,
+ base_meta=base_meta,
+ base_factory=base_factory,
+ )
- # 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, {}))
+ return new_class
- # 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)
+ def __str__(cls):
+ if cls._meta.abstract:
+ return '<%s (abstract)>' % cls.__name__
+ else:
+ return '<%s for %s>' % (cls.__name__, cls._meta.model)
- # 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
+class BaseMeta:
+ abstract = True
+ strategy = CREATE_STRATEGY
- 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
- attribute declaration from the class definition.
+class OptionDefault(object):
+ def __init__(self, name, value, inherit=False):
+ self.name = name
+ self.value = value
+ self.inherit = inherit
- 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
-
- Returns:
- A new class
- """
- parent_factories = get_factory_bases(bases)
- if not parent_factories:
- return super(FactoryMetaClass, mcs).__new__(
- mcs, class_name, bases, attrs)
-
- extra_attrs = {}
+ def apply(self, meta, base_meta):
+ value = self.value
+ if self.inherit and base_meta is not None:
+ value = getattr(base_meta, self.name, value)
+ if meta is not None:
+ value = getattr(meta, self.name, value)
+ return value
- is_abstract = attrs.pop('ABSTRACT_FACTORY', False)
+ def __str__(self):
+ return '%s(%r, %r, inherit=%r)' % (
+ self.__class__.__name__,
+ self.name, self.value, self.inherit)
- base = parent_factories[0]
- inherited_associated_class = base._get_target_class()
- associated_class = mcs._discover_associated_class(class_name, attrs,
- inherited_associated_class)
- # Invoke 'lazy-loading' hooks.
- associated_class = base._load_target_class(associated_class)
+class FactoryOptions(object):
+ def __init__(self):
+ self.factory = None
+ self.base_factory = None
+ self.declarations = {}
+ self.postgen_declarations = {}
- if associated_class is None:
- is_abstract = True
+ def _build_default_options(self):
+ """"Provide the default value for all allowed fields.
+ Custom FactoryOptions classes should override this method
+ to update() its return value.
+ """
+ return [
+ OptionDefault('model', None, inherit=True),
+ OptionDefault('abstract', False, inherit=False),
+ OptionDefault('strategy', CREATE_STRATEGY, inherit=True),
+ OptionDefault('inline_args', (), inherit=True),
+ OptionDefault('exclude', (), inherit=True),
+ OptionDefault('rename', {}, inherit=True),
+ ]
+
+ def _fill_from_meta(self, meta, base_meta):
+ # Exclude private/protected fields from the meta
+ if meta is None:
+ meta_attrs = {}
else:
- # If inheriting the factory from a parent, keep a link to it.
- # This allows to use the sequence counters from the parents.
- if (inherited_associated_class is not None
- and issubclass(associated_class, inherited_associated_class)):
- attrs['_base_factory'] = base
+ meta_attrs = dict((k, v)
+ for (k, v) in vars(meta).items()
+ if not k.startswith('_')
+ )
+
+ for option in self._build_default_options():
+ assert not hasattr(self, option.name), "Can't override field %s." % option.name
+ value = option.apply(meta, base_meta)
+ meta_attrs.pop(option.name, None)
+ setattr(self, option.name, value)
+
+ if meta_attrs:
+ # Some attributes in the Meta aren't allowed here
+ raise TypeError("'class Meta' for %r got unknown attribute(s) %s"
+ % (self.factory, ','.join(sorted(meta_attrs.keys()))))
+
+ def contribute_to_class(self, factory,
+ meta=None, base_meta=None, base_factory=None):
+
+ self.factory = factory
+ self.base_factory = base_factory
+
+ self._fill_from_meta(meta=meta, base_meta=base_meta)
+
+ self.model = self.factory._load_model_class(self.model)
+ if self.model is None:
+ self.abstract = True
+
+ self.counter_reference = self._get_counter_reference()
+
+ for parent in reversed(self.factory.__mro__[1:]):
+ if not hasattr(parent, '_meta'):
+ continue
+ self.declarations.update(parent._meta.declarations)
+ self.postgen_declarations.update(parent._meta.postgen_declarations)
+
+ for k, v in vars(self.factory).items():
+ if self._is_declaration(k, v):
+ self.declarations[k] = v
+ if self._is_postgen_declaration(k, v):
+ self.postgen_declarations[k] = v
+
+ def _get_counter_reference(self):
+ """Identify which factory should be used for a shared counter."""
+
+ if (self.model is not None
+ and self.base_factory is not None
+ and self.base_factory._meta.model is not None
+ and issubclass(self.model, self.base_factory._meta.model)):
+ return self.base_factory
+ else:
+ return self.factory
- # 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
+ def _is_declaration(self, name, value):
+ """Determines if a class attribute is a field value declaration.
- extra_attrs[CLASS_ATTRIBUTE_IS_ABSTRACT] = is_abstract
+ Based on the name and value of the class attribute, return ``True`` if
+ it looks like a declaration of a default field value, ``False`` if it
+ is private (name starts with '_') or a classmethod or staticmethod.
- # Extract pre- and post-generation declarations
- attributes = mcs._extract_declarations(parent_factories, attrs)
- attributes.update(extra_attrs)
+ """
+ if isinstance(value, (classmethod, staticmethod)):
+ return False
+ elif isinstance(value, declarations.OrderedDeclaration):
+ return True
+ elif isinstance(value, declarations.PostGenerationDeclaration):
+ return False
+ return not name.startswith("_")
- return super(FactoryMetaClass, mcs).__new__(
- mcs, class_name, bases, attributes)
+ def _is_postgen_declaration(self, name, value):
+ """Captures instances of PostGenerationDeclaration."""
+ return isinstance(value, declarations.PostGenerationDeclaration)
- def __str__(cls):
- if cls._abstract_factory:
- return '<%s (abstract)>'
- else:
- return '<%s for %s>' % (cls.__name__,
- getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__)
+ def __str__(self):
+ return "<%s for %s>" % (self.__class__.__name__, self.factory.__class__.__name__)
+
+ def __repr__(self):
+ return str(self)
# Factory base classes
@@ -252,26 +303,11 @@ class BaseFactory(object):
"""Would be called if trying to instantiate the class."""
raise FactoryError('You cannot instantiate BaseFactory')
+ _meta = FactoryOptions()
+
# ID to use for the next 'declarations.Sequence' attribute.
_counter = None
- # Base factory, if this class was inherited from another factory. This is
- # used for sharing the sequence _counter among factories for the same
- # class.
- _base_factory = None
-
- # 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 = ()
-
- # List of attributes that should not be passed to the underlying class
- FACTORY_HIDDEN_ARGS = ()
-
@classmethod
def reset_sequence(cls, value=None, force=False):
"""Reset the sequence counter.
@@ -282,9 +318,9 @@ class BaseFactory(object):
force (bool): whether to force-reset parent sequence counters
in a factory inheritance chain.
"""
- if cls._base_factory:
+ if cls._meta.counter_reference is not cls:
if force:
- cls._base_factory.reset_sequence(value=value)
+ cls._meta.base_factory.reset_sequence(value=value)
else:
raise ValueError(
"Cannot reset the sequence of a factory subclass. "
@@ -330,9 +366,9 @@ class BaseFactory(object):
"""
# Rely upon our parents
- if cls._base_factory and not cls._base_factory._abstract_factory:
- logger.debug("%r: reusing sequence from %r", cls, cls._base_factory)
- return cls._base_factory._generate_next_sequence()
+ if cls._meta.counter_reference is not cls:
+ logger.debug("%r: reusing sequence from %r", cls, cls._meta.base_factory)
+ return cls._meta.base_factory._generate_next_sequence()
# Make sure _counter is initialized
cls._setup_counter()
@@ -373,7 +409,15 @@ class BaseFactory(object):
extra_defs (dict): additional definitions to insert into the
retrieved DeclarationDict.
"""
- return getattr(cls, CLASS_ATTRIBUTE_DECLARATIONS).copy(extra_defs)
+ decls = cls._meta.declarations.copy()
+ decls.update(extra_defs or {})
+ return decls
+
+ @classmethod
+ def _rename_fields(cls, **kwargs):
+ for old_name, new_name in cls._meta.rename.items():
+ kwargs[new_name] = kwargs.pop(old_name)
+ return kwargs
@classmethod
def _adjust_kwargs(cls, **kwargs):
@@ -381,8 +425,8 @@ class BaseFactory(object):
return kwargs
@classmethod
- def _load_target_class(cls, class_definition):
- """Extension point for loading target classes.
+ def _load_model_class(cls, class_definition):
+ """Extension point for loading model classes.
This can be overridden in framework-specific subclasses to hook into
existing model repositories, for instance.
@@ -390,10 +434,10 @@ class BaseFactory(object):
return class_definition
@classmethod
- def _get_target_class(cls):
- """Retrieve the actual, associated target class."""
- definition = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None)
- return cls._load_target_class(definition)
+ def _get_model_class(cls):
+ """Retrieve the actual, associated model class."""
+ definition = cls._meta.model
+ return cls._load_model_class(definition)
@classmethod
def _prepare(cls, create, **kwargs):
@@ -403,15 +447,16 @@ class BaseFactory(object):
create: bool, whether to create or to build the object
**kwargs: arguments to pass to the creation function
"""
- target_class = cls._get_target_class()
+ model_class = cls._get_model_class()
+ kwargs = cls._rename_fields(**kwargs)
kwargs = cls._adjust_kwargs(**kwargs)
# Remove 'hidden' arguments.
- for arg in cls.FACTORY_HIDDEN_ARGS:
+ for arg in cls._meta.exclude:
del kwargs[arg]
# Extract *args from **kwargs
- args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS)
+ args = tuple(kwargs.pop(key) for key in cls._meta.inline_args)
logger.debug('BaseFactory: Generating %s.%s(%s)',
cls.__module__,
@@ -419,9 +464,9 @@ class BaseFactory(object):
utils.log_pprint(args, kwargs),
)
if create:
- return cls._create(target_class, *args, **kwargs)
+ return cls._create(model_class, *args, **kwargs)
else:
- return cls._build(target_class, *args, **kwargs)
+ return cls._build(model_class, *args, **kwargs)
@classmethod
def _generate(cls, create, attrs):
@@ -431,15 +476,14 @@ 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:
+ if cls._meta.abstract:
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))
+ "Ensure %(f)s.Meta.model is set and %(f)s.Meta.abstract "
+ "is either not set or False." % dict(f=cls.__name__))
# Extract declarations used for post-generation
- postgen_declarations = getattr(cls,
- CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS)
+ postgen_declarations = cls._meta.postgen_declarations
postgen_attributes = {}
for name, decl in sorted(postgen_declarations.items()):
postgen_attributes[name] = decl.extract(name, attrs)
@@ -469,34 +513,34 @@ class BaseFactory(object):
pass
@classmethod
- def _build(cls, target_class, *args, **kwargs):
- """Actually build an instance of the target_class.
+ def _build(cls, model_class, *args, **kwargs):
+ """Actually build an instance of the model_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
+ model_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)
+ return model_class(*args, **kwargs)
@classmethod
- def _create(cls, target_class, *args, **kwargs):
- """Actually create an instance of the target_class.
+ def _create(cls, model_class, *args, **kwargs):
+ """Actually create an instance of the model_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
+ model_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)
+ return model_class(*args, **kwargs)
@classmethod
def build(cls, **kwargs):
@@ -626,8 +670,7 @@ class BaseFactory(object):
Factory = FactoryMetaClass('Factory', (BaseFactory,), {
- 'ABSTRACT_FACTORY': True,
- 'FACTORY_STRATEGY': CREATE_STRATEGY,
+ 'Meta': BaseMeta,
'__doc__': """Factory base with build and create support.
This class has the ability to support multiple ORMs by using custom creation
@@ -642,12 +685,13 @@ Factory.AssociatedClassError = AssociatedClassError # pylint: disable=W0201
class StubFactory(Factory):
- FACTORY_STRATEGY = STUB_STRATEGY
- FACTORY_FOR = containers.StubObject
+ class Meta:
+ strategy = STUB_STRATEGY
+ model = containers.StubObject
@classmethod
def build(cls, **kwargs):
- raise UnsupportedStrategy()
+ return cls.stub(**kwargs)
@classmethod
def create(cls, **kwargs):
@@ -656,44 +700,48 @@ class StubFactory(Factory):
class BaseDictFactory(Factory):
"""Factory for dictionary-like classes."""
- ABSTRACT_FACTORY = True
+ class Meta:
+ abstract = True
@classmethod
- def _build(cls, target_class, *args, **kwargs):
+ def _build(cls, model_class, *args, **kwargs):
if args:
raise ValueError(
- "DictFactory %r does not support FACTORY_ARG_PARAMETERS.", cls)
- return target_class(**kwargs)
+ "DictFactory %r does not support Meta.inline_args.", cls)
+ return model_class(**kwargs)
@classmethod
- def _create(cls, target_class, *args, **kwargs):
- return cls._build(target_class, *args, **kwargs)
+ def _create(cls, model_class, *args, **kwargs):
+ return cls._build(model_class, *args, **kwargs)
class DictFactory(BaseDictFactory):
- FACTORY_FOR = dict
+ class Meta:
+ model = dict
class BaseListFactory(Factory):
"""Factory for list-like classes."""
- ABSTRACT_FACTORY = True
+ class Meta:
+ abstract = True
@classmethod
- def _build(cls, target_class, *args, **kwargs):
+ def _build(cls, model_class, *args, **kwargs):
if args:
raise ValueError(
- "ListFactory %r does not support FACTORY_ARG_PARAMETERS.", cls)
+ "ListFactory %r does not support Meta.inline_args.", cls)
values = [v for k, v in sorted(kwargs.items())]
- return target_class(values)
+ return model_class(values)
@classmethod
- def _create(cls, target_class, *args, **kwargs):
- return cls._build(target_class, *args, **kwargs)
+ def _create(cls, model_class, *args, **kwargs):
+ return cls._build(model_class, *args, **kwargs)
class ListFactory(BaseListFactory):
- FACTORY_FOR = list
+ class Meta:
+ model = list
def use_strategy(new_strategy):
@@ -702,6 +750,6 @@ def use_strategy(new_strategy):
This is an alternative to setting default_strategy in the class definition.
"""
def wrapped_class(klass):
- klass.FACTORY_STRATEGY = new_strategy
+ klass._meta.strategy = new_strategy
return klass
return wrapped_class
diff --git a/factory/compat.py b/factory/compat.py
index 7747b1a..737d91a 100644
--- a/factory/compat.py
+++ b/factory/compat.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 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
@@ -42,14 +42,6 @@ else: # pragma: no cover
from io import BytesIO
-if sys.version_info[:2] == (2, 6): # pragma: no cover
- def float_to_decimal(fl):
- return decimal.Decimal(str(fl))
-else: # pragma: no cover
- def float_to_decimal(fl):
- return decimal.Decimal(fl)
-
-
try: # pragma: no cover
# Python >= 3.2
UTC = datetime.timezone.utc
diff --git a/factory/containers.py b/factory/containers.py
index 4537e44..0ae354b 100644
--- a/factory/containers.py
+++ b/factory/containers.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 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
@@ -47,27 +47,27 @@ class LazyStub(object):
__containers (LazyStub list): "parents" of the LazyStub being built.
This allows to have the field of a field depend on the value of
another field
- __target_class (type): the target class to build.
+ __model_class (type): the model class to build.
"""
__initialized = False
- def __init__(self, attrs, containers=(), target_class=object, log_ctx=None):
+ def __init__(self, attrs, containers=(), model_class=object, log_ctx=None):
self.__attrs = attrs
self.__values = {}
self.__pending = []
self.__containers = containers
- self.__target_class = target_class
- self.__log_ctx = log_ctx or '%s.%s' % (target_class.__module__, target_class.__name__)
+ self.__model_class = model_class
+ self.__log_ctx = log_ctx or '%s.%s' % (model_class.__module__, model_class.__name__)
self.factory_parent = containers[0] if containers else None
self.__initialized = True
def __repr__(self):
- return '<LazyStub for %s.%s>' % (self.__target_class.__module__, self.__target_class.__name__)
+ return '<LazyStub for %s.%s>' % (self.__model_class.__module__, self.__model_class.__name__)
def __str__(self):
return '<LazyStub for %s with %s>' % (
- self.__target_class.__name__, list(self.__attrs.keys()))
+ self.__model_class.__name__, list(self.__attrs.keys()))
def __fill__(self):
"""Fill this LazyStub, computing values of all defined attributes.
@@ -121,61 +121,6 @@ class LazyStub(object):
raise AttributeError('Setting of object attributes is not allowed')
-class DeclarationDict(dict):
- """Slightly extended dict to work with OrderedDeclaration."""
-
- def is_declaration(self, name, value):
- """Determines if a class attribute is a field value declaration.
-
- Based on the name and value of the class attribute, return ``True`` if
- it looks like a declaration of a default field value, ``False`` if it
- is private (name starts with '_') or a classmethod or staticmethod.
-
- """
- if isinstance(value, (classmethod, staticmethod)):
- return False
- elif isinstance(value, declarations.OrderedDeclaration):
- return True
- return (not name.startswith("_") and not name.startswith("FACTORY_"))
-
- def update_with_public(self, d):
- """Updates the DeclarationDict from a class definition dict.
-
- Takes into account all public attributes and OrderedDeclaration
- instances; ignores all class/staticmethods and private attributes
- (starting with '_').
-
- Returns a dict containing all remaining elements.
- """
- remaining = {}
- for k, v in d.items():
- if self.is_declaration(k, v):
- self[k] = v
- else:
- remaining[k] = v
- return remaining
-
- def copy(self, extra=None):
- """Copy this DeclarationDict into another one, including extra values.
-
- Args:
- extra (dict): additional attributes to include in the copy.
- """
- 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."""
@@ -279,7 +224,7 @@ class AttributeBuilder(object):
wrapped_attrs[k] = v
stub = LazyStub(wrapped_attrs, containers=self._containers,
- target_class=self.factory, log_ctx=self._log_ctx)
+ model_class=self.factory, log_ctx=self._log_ctx)
return stub.__fill__()
diff --git a/factory/declarations.py b/factory/declarations.py
index 037a679..f0dbfe5 100644
--- a/factory/declarations.py
+++ b/factory/declarations.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 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
@@ -22,7 +22,6 @@
import itertools
-import warnings
import logging
from . import compat
@@ -50,7 +49,7 @@ 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
+ create (bool): whether the model class should be 'built' or
'created'
extra (DeclarationDict or None): extracted key/value extracted from
the attribute prefix
@@ -161,12 +160,19 @@ class Iterator(OrderedDeclaration):
def __init__(self, iterator, cycle=True, getter=None):
super(Iterator, self).__init__()
self.getter = getter
+ self.iterator = None
if cycle:
- iterator = itertools.cycle(iterator)
- self.iterator = utils.ResetableIterator(iterator)
+ self.iterator_builder = lambda: utils.ResetableIterator(itertools.cycle(iterator))
+ else:
+ self.iterator_builder = lambda: utils.ResetableIterator(iterator)
def evaluate(self, sequence, obj, create, extra=None, containers=()):
+ # Begin unrolling as late as possible.
+ # This helps with ResetableIterator(MyModel.objects.all())
+ if self.iterator is None:
+ self.iterator = self.iterator_builder()
+
logger.debug("Iterator: Fetching next value from %r", self.iterator)
value = next(iter(self.iterator))
if self.getter is None:
@@ -195,7 +201,7 @@ class Sequence(OrderedDeclaration):
self.type = type
def evaluate(self, sequence, obj, create, extra=None, containers=()):
- logger.debug("Sequence: Computing next value of %r for seq=%d", self.function, sequence)
+ logger.debug("Sequence: Computing next value of %r for seq=%s", self.function, sequence)
return self.function(self.type(sequence))
@@ -209,7 +215,7 @@ class LazyAttributeSequence(Sequence):
of counter for the 'function' attribute.
"""
def evaluate(self, sequence, obj, create, extra=None, containers=()):
- logger.debug("LazyAttributeSequence: Computing next value of %r for seq=%d, obj=%r",
+ logger.debug("LazyAttributeSequence: Computing next value of %r for seq=%s, obj=%r",
self.function, sequence, obj)
return self.function(obj, self.type(sequence))
@@ -434,7 +440,7 @@ class ExtractionContext(object):
class PostGenerationDeclaration(object):
- """Declarations to be called once the target object has been generated."""
+ """Declarations to be called once the model object has been generated."""
def extract(self, name, attrs):
"""Extract relevant attributes from a dict.
@@ -502,14 +508,6 @@ class RelatedFactory(PostGenerationDeclaration):
def __init__(self, factory, factory_related_name='', **defaults):
super(RelatedFactory, self).__init__()
- if factory_related_name == '' and defaults.get('name') is not None:
- warnings.warn(
- "Usage of RelatedFactory(SomeFactory, name='foo') is deprecated"
- " and will be removed in the future. Please use the"
- " RelatedFactory(SomeFactory, 'foo') or"
- " RelatedFactory(SomeFactory, factory_related_name='foo')"
- " syntax instead", PendingDeprecationWarning, 2)
- factory_related_name = defaults.pop('name')
self.name = factory_related_name
self.defaults = defaults
diff --git a/factory/django.py b/factory/django.py
index a3dfdfc..b3c508c 100644
--- a/factory/django.py
+++ b/factory/django.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 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
@@ -32,8 +32,10 @@ import functools
"""factory_boy extensions for use with the Django framework."""
try:
+ import django
from django.core import files as django_files
except ImportError as e: # pragma: no cover
+ django = None
django_files = None
import_failure = e
@@ -45,6 +47,8 @@ from .compat import BytesIO, is_string
logger = logging.getLogger('factory.generate')
+DEFAULT_DB_ALIAS = 'default' # Same as django.db.DEFAULT_DB_ALIAS
+
def require_django():
"""Simple helper to ensure Django is available."""
@@ -52,6 +56,56 @@ def require_django():
raise import_failure
+_LAZY_LOADS = {}
+
+def get_model(app, model):
+ """Wrapper around django's get_model."""
+ if 'get_model' not in _LAZY_LOADS:
+ _lazy_load_get_model()
+
+ _get_model = _LAZY_LOADS['get_model']
+ return _get_model(app, model)
+
+
+def _lazy_load_get_model():
+ """Lazy loading of get_model.
+
+ get_model loads django.conf.settings, which may fail if
+ the settings haven't been configured yet.
+ """
+ if django is None:
+ def get_model(app, model):
+ raise import_failure
+
+ elif django.VERSION[:2] < (1, 7):
+ from django.db.models.loading import get_model
+
+ else:
+ from django import apps as django_apps
+ get_model = django_apps.apps.get_model
+ _LAZY_LOADS['get_model'] = get_model
+
+
+class DjangoOptions(base.FactoryOptions):
+ def _build_default_options(self):
+ return super(DjangoOptions, self)._build_default_options() + [
+ base.OptionDefault('django_get_or_create', (), inherit=True),
+ base.OptionDefault('database', DEFAULT_DB_ALIAS, inherit=True),
+ ]
+
+ def _get_counter_reference(self):
+ counter_reference = super(DjangoOptions, self)._get_counter_reference()
+ if (counter_reference == self.base_factory
+ and self.base_factory._meta.model is not None
+ and self.base_factory._meta.model._meta.abstract
+ and self.model is not None
+ and not self.model._meta.abstract):
+ # Target factory is for an abstract model, yet we're for another,
+ # concrete subclass => don't reuse the counter.
+ return self.factory
+ return counter_reference
+
+
class DjangoModelFactory(base.Factory):
"""Factory for Django models.
@@ -61,53 +115,48 @@ class DjangoModelFactory(base.Factory):
handle those for non-numerical primary keys.
"""
- ABSTRACT_FACTORY = True # Optional, but explicit.
- FACTORY_DJANGO_GET_OR_CREATE = ()
+ _options_class = DjangoOptions
+ class Meta:
+ abstract = True # Optional, but explicit.
@classmethod
- def _load_target_class(cls, definition):
+ def _load_model_class(cls, definition):
if is_string(definition) and '.' in definition:
app, model = definition.split('.', 1)
- from django.db.models import loading as django_loading
- return django_loading.get_model(app, model)
+ return get_model(app, model)
return definition
@classmethod
- def _get_manager(cls, target_class):
+ def _get_manager(cls, model_class):
+ if model_class is None:
+ raise base.AssociatedClassError("No model set on %s.%s.Meta"
+ % (cls.__module__, cls.__name__))
+
try:
- return target_class._default_manager # pylint: disable=W0212
+ manager = model_class.objects
except AttributeError:
- return target_class.objects
-
- @classmethod
- def _setup_next_sequence(cls):
- """Compute the next available PK, based on the 'pk' database field."""
+ # When inheriting from an abstract model with a custom
+ # manager, the class has no 'objects' field.
+ manager = model_class._default_manager
- model = cls._get_target_class() # pylint: disable=E1101
- manager = cls._get_manager(model)
-
- try:
- return 1 + manager.values_list('pk', flat=True
- ).order_by('-pk')[0]
- except (IndexError, TypeError):
- # IndexError: No instance exist yet
- # TypeError: pk isn't an integer type
- return 1
+ if cls._meta.database != DEFAULT_DB_ALIAS:
+ manager = manager.using(cls._meta.database)
+ return manager
@classmethod
- def _get_or_create(cls, target_class, *args, **kwargs):
+ def _get_or_create(cls, model_class, *args, **kwargs):
"""Create an instance of the model through objects.get_or_create."""
- manager = cls._get_manager(target_class)
+ manager = cls._get_manager(model_class)
- assert 'defaults' not in cls.FACTORY_DJANGO_GET_OR_CREATE, (
+ assert 'defaults' not in cls._meta.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))
+ "(in %s._meta.django_get_or_create=%r)"
+ % (cls, cls._meta.django_get_or_create))
key_fields = {}
- for field in cls.FACTORY_DJANGO_GET_OR_CREATE:
+ for field in cls._meta.django_get_or_create:
key_fields[field] = kwargs.pop(field)
key_fields['defaults'] = kwargs
@@ -115,12 +164,12 @@ class DjangoModelFactory(base.Factory):
return obj
@classmethod
- def _create(cls, target_class, *args, **kwargs):
+ def _create(cls, model_class, *args, **kwargs):
"""Create an instance of the model, and save it to the database."""
- manager = cls._get_manager(target_class)
+ manager = cls._get_manager(model_class)
- if cls.FACTORY_DJANGO_GET_OR_CREATE:
- return cls._get_or_create(target_class, *args, **kwargs)
+ if cls._meta.django_get_or_create:
+ return cls._get_or_create(model_class, *args, **kwargs)
return manager.create(*args, **kwargs)
@@ -132,24 +181,22 @@ class DjangoModelFactory(base.Factory):
obj.save()
-class FileField(declarations.PostGenerationDeclaration):
+class FileField(declarations.ParameteredAttribute):
"""Helper to fill in django.db.models.FileField from a Factory."""
DEFAULT_FILENAME = 'example.dat'
+ EXTEND_CONTAINERS = True
def __init__(self, **defaults):
require_django()
- self.defaults = defaults
- super(FileField, self).__init__()
+ super(FileField, self).__init__(**defaults)
def _make_data(self, params):
"""Create data for the field."""
return params.get('data', b'')
- def _make_content(self, extraction_context):
+ def _make_content(self, params):
path = ''
- params = dict(self.defaults)
- params.update(extraction_context.extra)
if params.get('from_path') and params.get('from_file'):
raise ValueError(
@@ -157,12 +204,7 @@ class FileField(declarations.PostGenerationDeclaration):
"be non-empty when calling factory.django.FileField."
)
- if extraction_context.did_extract:
- # Should be a django.core.files.File
- content = extraction_context.value
- path = content.name
-
- elif params.get('from_path'):
+ if params.get('from_path'):
path = params['from_path']
f = open(path, 'rb')
content = django_files.File(f, name=path)
@@ -184,19 +226,13 @@ class FileField(declarations.PostGenerationDeclaration):
filename = params.get('filename', default_filename)
return filename, content
- def call(self, obj, create, extraction_context):
+ def generate(self, sequence, obj, create, params):
"""Fill in the field."""
- if extraction_context.did_extract and extraction_context.value is None:
- # User passed an empty value, don't fill
- return
- filename, content = self._make_content(extraction_context)
- field_file = getattr(obj, extraction_context.for_field)
- try:
- field_file.save(filename, content, save=create)
- finally:
- content.file.close()
- return field_file
+ params.setdefault('__sequence', sequence)
+ params = base.DictFactory.simple_generate(create, **params)
+ filename, content = self._make_content(params)
+ return django_files.File(content.file, filename)
class ImageField(FileField):
@@ -250,6 +286,9 @@ class mute_signals(object):
logger.debug('mute_signals: Disabling signal handlers %r',
signal.receivers)
+ # Note that we're using implementation details of
+ # django.signals, since arguments to signal.connect()
+ # are lost in signal.receivers
self.paused[signal] = signal.receivers
signal.receivers = []
@@ -259,8 +298,17 @@ class mute_signals(object):
receivers)
signal.receivers = receivers
+ if django.VERSION[:2] >= (1, 6):
+ with signal.lock:
+ # Django uses some caching for its signals.
+ # Since we're bypassing signal.connect and signal.disconnect,
+ # we have to keep messing with django's internals.
+ signal.sender_receivers_cache.clear()
self.paused = {}
+ def copy(self):
+ return mute_signals(*self.signals)
+
def __call__(self, callable_obj):
if isinstance(callable_obj, base.FactoryMetaClass):
# Retrieve __func__, the *actual* callable object.
@@ -269,7 +317,8 @@ class mute_signals(object):
@classmethod
@functools.wraps(generate_method)
def wrapped_generate(*args, **kwargs):
- with self:
+ # A mute_signals() object is not reentrant; use a copy everytime.
+ with self.copy():
return generate_method(*args, **kwargs)
callable_obj._generate = wrapped_generate
@@ -278,7 +327,8 @@ class mute_signals(object):
else:
@functools.wraps(callable_obj)
def wrapper(*args, **kwargs):
- with self:
+ # A mute_signals() object is not reentrant; use a copy everytime.
+ with self.copy():
return callable_obj(*args, **kwargs)
return wrapper
diff --git a/factory/faker.py b/factory/faker.py
new file mode 100644
index 0000000..5411985
--- /dev/null
+++ b/factory/faker.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Mark Sandstrom
+# Copyright (c) 2011-2015 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 "faker" attributes.
+
+Usage:
+
+ class MyFactory(factory.Factory):
+ class Meta:
+ model = MyProfile
+
+ first_name = factory.Faker('name')
+"""
+
+
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import contextlib
+
+import faker
+import faker.config
+
+from . import declarations
+
+class Faker(declarations.OrderedDeclaration):
+ """Wrapper for 'faker' values.
+
+ Args:
+ provider (str): the name of the Faker field
+ locale (str): the locale to use for the faker
+
+ All other kwargs will be passed to the underlying provider
+ (e.g ``factory.Faker('ean', length=10)``
+ calls ``faker.Faker.ean(length=10)``)
+
+ Usage:
+ >>> foo = factory.Faker('name')
+ """
+ def __init__(self, provider, locale=None, **kwargs):
+ self.provider = provider
+ self.provider_kwargs = kwargs
+ self.locale = locale
+
+ def generate(self, extra_kwargs):
+ kwargs = {}
+ kwargs.update(self.provider_kwargs)
+ kwargs.update(extra_kwargs)
+ faker = self._get_faker(self.locale)
+ return faker.format(self.provider, **kwargs)
+
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
+ return self.generate(extra or {})
+
+ _FAKER_REGISTRY = {}
+ _DEFAULT_LOCALE = faker.config.DEFAULT_LOCALE
+
+ @classmethod
+ @contextlib.contextmanager
+ def override_default_locale(cls, locale):
+ old_locale = cls._DEFAULT_LOCALE
+ cls._DEFAULT_LOCALE = locale
+ try:
+ yield
+ finally:
+ cls._DEFAULT_LOCALE = old_locale
+
+ @classmethod
+ def _get_faker(cls, locale=None):
+ if locale is None:
+ locale = cls._DEFAULT_LOCALE
+
+ if locale not in cls._FAKER_REGISTRY:
+ cls._FAKER_REGISTRY[locale] = faker.Faker(locale=locale)
+
+ return cls._FAKER_REGISTRY[locale]
+
+ @classmethod
+ def add_provider(cls, provider, locale=None):
+ """Add a new Faker provider for the specified locale"""
+ cls._get_faker(locale).add_provider(provider)
diff --git a/factory/fuzzy.py b/factory/fuzzy.py
index 94599b7..a7e834c 100644
--- a/factory/fuzzy.py
+++ b/factory/fuzzy.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 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
@@ -34,6 +34,25 @@ from . import compat
from . import declarations
+_random = random.Random()
+
+
+def get_random_state():
+ """Retrieve the state of factory.fuzzy's random generator."""
+ return _random.getstate()
+
+
+def set_random_state(state):
+ """Force-set the state of factory.fuzzy's random generator."""
+ return _random.setstate(state)
+
+
+def reseed_random(seed):
+ """Reseed factory.fuzzy's random generator."""
+ r = random.Random(seed)
+ set_random_state(r.getstate())
+
+
class BaseFuzzyAttribute(declarations.OrderedDeclaration):
"""Base class for fuzzy attributes.
@@ -81,7 +100,7 @@ class FuzzyText(BaseFuzzyAttribute):
"""
def __init__(self, prefix='', length=12, suffix='',
- chars=string.ascii_letters, **kwargs):
+ chars=string.ascii_letters, **kwargs):
super(FuzzyText, self).__init__(**kwargs)
self.prefix = prefix
self.suffix = suffix
@@ -89,19 +108,27 @@ class FuzzyText(BaseFuzzyAttribute):
self.chars = tuple(chars) # Unroll iterators
def fuzz(self):
- chars = [random.choice(self.chars) for _i in range(self.length)]
+ chars = [_random.choice(self.chars) for _i in range(self.length)]
return self.prefix + ''.join(chars) + self.suffix
class FuzzyChoice(BaseFuzzyAttribute):
- """Handles fuzzy choice of an attribute."""
+ """Handles fuzzy choice of an attribute.
+
+ Args:
+ choices (iterable): An iterable yielding options; will only be unrolled
+ on the first call.
+ """
def __init__(self, choices, **kwargs):
- self.choices = list(choices)
+ self.choices = None
+ self.choices_generator = choices
super(FuzzyChoice, self).__init__(**kwargs)
def fuzz(self):
- return random.choice(self.choices)
+ if self.choices is None:
+ self.choices = list(self.choices_generator)
+ return _random.choice(self.choices)
class FuzzyInteger(BaseFuzzyAttribute):
@@ -119,7 +146,7 @@ class FuzzyInteger(BaseFuzzyAttribute):
super(FuzzyInteger, self).__init__(**kwargs)
def fuzz(self):
- return random.randrange(self.low, self.high + 1, self.step)
+ return _random.randrange(self.low, self.high + 1, self.step)
class FuzzyDecimal(BaseFuzzyAttribute):
@@ -137,7 +164,7 @@ class FuzzyDecimal(BaseFuzzyAttribute):
super(FuzzyDecimal, self).__init__(**kwargs)
def fuzz(self):
- base = compat.float_to_decimal(random.uniform(self.low, self.high))
+ base = decimal.Decimal(str(_random.uniform(self.low, self.high)))
return base.quantize(decimal.Decimal(10) ** -self.precision)
@@ -155,7 +182,7 @@ class FuzzyFloat(BaseFuzzyAttribute):
super(FuzzyFloat, self).__init__(**kwargs)
def fuzz(self):
- return random.uniform(self.low, self.high)
+ return _random.uniform(self.low, self.high)
class FuzzyDate(BaseFuzzyAttribute):
@@ -175,7 +202,7 @@ class FuzzyDate(BaseFuzzyAttribute):
self.end_date = end_date.toordinal()
def fuzz(self):
- return datetime.date.fromordinal(random.randint(self.start_date, self.end_date))
+ return datetime.date.fromordinal(_random.randint(self.start_date, self.end_date))
class BaseFuzzyDateTime(BaseFuzzyAttribute):
@@ -215,7 +242,7 @@ class BaseFuzzyDateTime(BaseFuzzyAttribute):
delta = self.end_dt - self.start_dt
microseconds = delta.microseconds + 1000000 * (delta.seconds + (delta.days * 86400))
- offset = random.randint(0, microseconds)
+ offset = _random.randint(0, microseconds)
result = self.start_dt + datetime.timedelta(microseconds=offset)
if self.force_year is not None:
@@ -270,10 +297,10 @@ class FuzzyDateTime(BaseFuzzyDateTime):
def _check_bounds(self, start_dt, end_dt):
if start_dt.tzinfo is None:
raise ValueError(
- "FuzzyDateTime only handles aware datetimes, got start=%r"
+ "FuzzyDateTime requires timezone-aware datetimes, got start=%r"
% start_dt)
if end_dt.tzinfo is None:
raise ValueError(
- "FuzzyDateTime only handles aware datetimes, got end=%r"
+ "FuzzyDateTime requires timezone-aware datetimes, got end=%r"
% end_dt)
super(FuzzyDateTime, self)._check_bounds(start_dt, end_dt)
diff --git a/factory/helpers.py b/factory/helpers.py
index 4a2a254..60a4d75 100644
--- a/factory/helpers.py
+++ b/factory/helpers.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 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
@@ -28,7 +28,6 @@ import logging
from . import base
from . import declarations
-from . import django
@contextlib.contextmanager
@@ -50,7 +49,9 @@ def debug(logger='factory', stream=None):
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
+ class Meta:
+ model = klass
+ kwargs['Meta'] = Meta
base_class = kwargs.pop('FACTORY_CLASS', base.Factory)
factory_class = type(base.Factory).__new__(
diff --git a/factory/mogo.py b/factory/mogo.py
index 48d9677..aa9f28b 100644
--- a/factory/mogo.py
+++ b/factory/mogo.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 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
@@ -32,14 +32,15 @@ from . import base
class MogoFactory(base.Factory):
"""Factory for mogo objects."""
- ABSTRACT_FACTORY = True
+ class Meta:
+ abstract = True
@classmethod
- def _build(cls, target_class, *args, **kwargs):
- return target_class.new(*args, **kwargs)
+ def _build(cls, model_class, *args, **kwargs):
+ return model_class(*args, **kwargs)
@classmethod
- def _create(cls, target_class, *args, **kwargs):
- instance = target_class.new(*args, **kwargs)
+ def _create(cls, model_class, *args, **kwargs):
+ instance = model_class(*args, **kwargs)
instance.save()
return instance
diff --git a/factory/mongoengine.py b/factory/mongoengine.py
index 462f5f2..f50b727 100644
--- a/factory/mongoengine.py
+++ b/factory/mongoengine.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 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
@@ -32,15 +32,17 @@ from . import base
class MongoEngineFactory(base.Factory):
"""Factory for mongoengine objects."""
- ABSTRACT_FACTORY = True
+
+ class Meta:
+ abstract = True
@classmethod
- def _build(cls, target_class, *args, **kwargs):
- return target_class(*args, **kwargs)
+ def _build(cls, model_class, *args, **kwargs):
+ return model_class(*args, **kwargs)
@classmethod
- def _create(cls, target_class, *args, **kwargs):
- instance = target_class(*args, **kwargs)
+ def _create(cls, model_class, *args, **kwargs):
+ instance = model_class(*args, **kwargs)
if instance._is_document:
instance.save()
return instance
diff --git a/factory/utils.py b/factory/utils.py
index 6f0c763..15dba0a 100644
--- a/factory/utils.py
+++ b/factory/utils.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 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
@@ -110,15 +110,29 @@ def _safe_repr(obj):
return obj_repr.decode('utf-8')
-def log_pprint(args=(), kwargs=None):
- kwargs = kwargs or {}
- return ', '.join(
- [_safe_repr(arg) for arg in args] +
- [
- '%s=%s' % (key, _safe_repr(value))
- for key, value in kwargs.items()
- ]
- )
+class log_pprint(object):
+ """Helper for properly printing args / kwargs passed to an object.
+
+ Since it is only used with factory.debug(), the computation is
+ performed lazily.
+ """
+ __slots__ = ['args', 'kwargs']
+
+ def __init__(self, args=(), kwargs=None):
+ self.args = args
+ self.kwargs = kwargs or {}
+
+ def __repr__(self):
+ return repr(str(self))
+
+ def __str__(self):
+ return ', '.join(
+ [_safe_repr(arg) for arg in self.args] +
+ [
+ '%s=%s' % (key, _safe_repr(value))
+ for key, value in self.kwargs.items()
+ ]
+ )
class ResetableIterator(object):