diff options
Diffstat (limited to 'factory')
-rw-r--r-- | factory/__init__.py | 4 | ||||
-rw-r--r-- | factory/alchemy.py | 27 | ||||
-rw-r--r-- | factory/base.py | 424 | ||||
-rw-r--r-- | factory/containers.py | 69 | ||||
-rw-r--r-- | factory/declarations.py | 4 | ||||
-rw-r--r-- | factory/django.py | 130 | ||||
-rw-r--r-- | factory/fuzzy.py | 22 | ||||
-rw-r--r-- | factory/helpers.py | 5 | ||||
-rw-r--r-- | factory/mogo.py | 11 | ||||
-rw-r--r-- | factory/mongoengine.py | 12 |
10 files changed, 427 insertions, 281 deletions
diff --git a/factory/__init__.py b/factory/__init__.py index aa550e8..8fc8ef8 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -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.4.1' __author__ = 'Raphaƫl Barrois <raphael.barrois+fboy@polytechnique.org>' @@ -32,6 +32,8 @@ from .base import ( ListFactory, StubFactory, + FactoryError, + BUILD_STRATEGY, CREATE_STRATEGY, STUB_STRATEGY, diff --git a/factory/alchemy.py b/factory/alchemy.py index cec15c9..3c91411 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -24,17 +24,30 @@ 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), + ] + + class SQLAlchemyModelFactory(base.Factory): """Factory for SQLAlchemy models. """ - ABSTRACT_FACTORY = True - FACTORY_SESSION = None + _options_class = SQLAlchemyOptions + class Meta: + abstract = True + + _OLDSTYLE_ATTRIBUTES = base.Factory._OLDSTYLE_ATTRIBUTES.copy() + _OLDSTYLE_ATTRIBUTES.update({ + 'FACTORY_SESSION': 'sqlalchemy_session', + }) @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 + session = cls._meta.sqlalchemy_session + model = cls._meta.model pk = getattr(model, model.__mapper__.primary_key[0].name) max_pk = session.query(max(pk)).one()[0] if isinstance(max_pk, int): @@ -43,9 +56,9 @@ class SQLAlchemyModelFactory(base.Factory): return 1 @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) return obj diff --git a/factory/base.py b/factory/base.py index 3c6571c..9e07899 100644 --- a/factory/base.py +++ b/factory/base.py @@ -21,8 +21,10 @@ # THE SOFTWARE. import logging +import warnings from . import containers +from . import declarations from . import utils logger = logging.getLogger('factory.generate') @@ -33,22 +35,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 +59,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,80 +76,15 @@ 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)) - - @classmethod - 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 an inherited associated class was provided, use it. - - Args: - class_name (str): the name of the factory class being created - attrs (dict): the dict of attributes from the factory class - definition - inherited (class): the optional associated class inherited from a - parent factory - - Returns: - class: the class to associate with this factory - """ - if FACTORY_CLASS_DECLARATION in attrs: - return attrs[FACTORY_CLASS_DECLARATION] - - # No specific associated class was given, and one was defined for our - # parent, use it. - if inherited is not None: - return inherited - - # Nothing found, return None. - return None - - @classmethod - def _extract_declarations(mcs, bases, attributes): - """Extract declarations from a class definition. - - 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 + raise UnknownStrategy('Unknown Meta.strategy: {0}'.format( + cls._meta.strategy)) def __new__(mcs, class_name, bases, attrs): """Record attributes as a pattern for later instance construction. @@ -166,51 +102,183 @@ class FactoryMetaClass(type): A new class """ parent_factories = get_factory_bases(bases) - if not parent_factories: - return super(FactoryMetaClass, mcs).__new__( - mcs, class_name, bases, attrs) + if parent_factories: + base_factory = parent_factories[0] + else: + base_factory = None + + attrs_meta = attrs.pop('Meta', None) + + oldstyle_attrs = {} + converted_attrs = {} + for old_name, new_name in base_factory._OLDSTYLE_ATTRIBUTES.items(): + if old_name in attrs: + oldstyle_attrs[old_name] = new_name + converted_attrs[new_name] = attrs.pop(old_name) + if oldstyle_attrs: + warnings.warn( + "Declaring any of %s at class-level is deprecated" + " and will be removed in the future. Please set them" + " as %s attributes of a 'class Meta' attribute." % ( + ', '.join(oldstyle_attrs.keys()), + ', '.join(oldstyle_attrs.values()), + ), + PendingDeprecationWarning, 2) + attrs_meta = type('Meta', (object,), converted_attrs) + + base_meta = resolve_attribute('_meta', bases) + options_class = resolve_attribute('_options_class', bases, FactoryOptions) + + meta = options_class() + attrs['_meta'] = meta + + new_class = super(FactoryMetaClass, mcs).__new__( + mcs, class_name, bases, attrs) + + meta.contribute_to_class(new_class, + meta=attrs_meta, + base_meta=base_meta, + base_factory=base_factory, + ) - extra_attrs = {} + return new_class - is_abstract = attrs.pop('ABSTRACT_FACTORY', False) + def __str__(cls): + if cls._meta.abstract: + return '<%s (abstract)>' % cls.__name__ + else: + return '<%s for %s>' % (cls.__name__, cls._meta.model) - 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 BaseMeta: + abstract = True + strategy = CREATE_STRATEGY - if associated_class is None: - is_abstract = True - else: - # If inheriting the factory from a parent, keep a link to it. - # This allows to use the sequence counters from the parents. - if (inherited_associated_class is not None - and issubclass(associated_class, inherited_associated_class)): - attrs['_base_factory'] = base +class OptionDefault(object): + def __init__(self, name, value, inherit=False): + self.name = name + self.value = value + self.inherit = inherit - # 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 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 - extra_attrs[CLASS_ATTRIBUTE_IS_ABSTRACT] = is_abstract + def __str__(self): + return '%s(%r, %r, inherit=%r)' % ( + self.__class__.__name__, + self.name, self.value, self.inherit) - # Extract pre- and post-generation declarations - attributes = mcs._extract_declarations(parent_factories, attrs) - attributes.update(extra_attrs) - return super(FactoryMetaClass, mcs).__new__( - mcs, class_name, bases, attributes) +class FactoryOptions(object): + def __init__(self): + self.factory = None + self.base_factory = None + self.declarations = {} + self.postgen_declarations = {} - def __str__(cls): - if cls._abstract_factory: - return '<%s (abstract)>' + 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), + ] + + def _fill_from_meta(self, meta, base_meta): + # Exclude private/protected fields from the meta + if meta is None: + meta_attrs = {} + else: + 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 '<%s for %s>' % (cls.__name__, - getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__) + return self.factory + + 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 + elif isinstance(value, declarations.PostGenerationDeclaration): + return False + return not name.startswith("_") + + def _is_postgen_declaration(self, name, value): + """Captures instances of PostGenerationDeclaration.""" + return isinstance(value, declarations.PostGenerationDeclaration) + + def __str__(self): + return "<%s for %s>" % (self.__class__.__name__, self.factory.__class__.__name__) + + def __repr__(self): + return str(self) # Factory base classes @@ -252,25 +320,18 @@ class BaseFactory(object): """Would be called if trying to instantiate the class.""" raise FactoryError('You cannot instantiate BaseFactory') - # 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 + _meta = FactoryOptions() - # Whether this factory is considered "abstract", thus uncallable. - _abstract_factory = False + _OLDSTYLE_ATTRIBUTES = { + 'FACTORY_FOR': 'model', + 'ABSTRACT_FACTORY': 'abstract', + 'FACTORY_STRATEGY': 'strategy', + 'FACTORY_ARG_PARAMETERS': 'inline_args', + 'FACTORY_HIDDEN_ARGS': 'exclude', + } - # 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 = () + # ID to use for the next 'declarations.Sequence' attribute. + _counter = None @classmethod def reset_sequence(cls, value=None, force=False): @@ -282,9 +343,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 +391,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 +434,9 @@ 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) + return decls @classmethod def _adjust_kwargs(cls, **kwargs): @@ -381,8 +444,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 +453,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 +466,15 @@ 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._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 +482,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 +494,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 +531,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 +688,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,8 +703,9 @@ 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): @@ -656,44 +718,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 +768,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/containers.py b/factory/containers.py index 4537e44..5116320 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -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..5e7e734 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -50,7 +50,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 @@ -434,7 +434,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. diff --git a/factory/django.py b/factory/django.py index fee8e52..2b6c463 100644 --- a/factory/django.py +++ b/factory/django.py @@ -25,6 +25,9 @@ from __future__ import absolute_import from __future__ import unicode_literals import os +import types +import logging +import functools """factory_boy extensions for use with the Django framework.""" @@ -39,6 +42,9 @@ from . import base from . import declarations from .compat import BytesIO, is_string +logger = logging.getLogger('factory.generate') + + def require_django(): """Simple helper to ensure Django is available.""" @@ -46,6 +52,25 @@ def require_django(): raise import_failure +class DjangoOptions(base.FactoryOptions): + def _build_default_options(self): + return super(DjangoOptions, self)._build_default_options() + [ + base.OptionDefault('django_get_or_create', (), 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. @@ -55,11 +80,17 @@ 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. + + _OLDSTYLE_ATTRIBUTES = base.Factory._OLDSTYLE_ATTRIBUTES.copy() + _OLDSTYLE_ATTRIBUTES.update({ + 'FACTORY_DJANGO_GET_OR_CREATE': 'django_get_or_create', + }) @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) @@ -69,17 +100,20 @@ class DjangoModelFactory(base.Factory): 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 + return model_class._default_manager # pylint: disable=W0212 except AttributeError: - return target_class.objects + return model_class.objects @classmethod def _setup_next_sequence(cls): """Compute the next available PK, based on the 'pk' database field.""" - model = cls._get_target_class() # pylint: disable=E1101 + model = cls._get_model_class() # pylint: disable=E1101 manager = cls._get_manager(model) try: @@ -91,17 +125,17 @@ class DjangoModelFactory(base.Factory): return 1 @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 @@ -109,12 +143,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) @@ -214,3 +248,65 @@ class ImageField(FileField): thumb.save(thumb_io, format=image_format) return thumb_io.getvalue() + +class mute_signals(object): + """Temporarily disables and then restores any django signals. + + Args: + *signals (django.dispatch.dispatcher.Signal): any django signals + + Examples: + with mute_signals(pre_init): + user = UserFactory.build() + ... + + @mute_signals(pre_save, post_save) + class UserFactory(factory.Factory): + ... + + @mute_signals(post_save) + def generate_users(): + UserFactory.create_batch(10) + """ + + def __init__(self, *signals): + self.signals = signals + self.paused = {} + + def __enter__(self): + for signal in self.signals: + logger.debug('mute_signals: Disabling signal handlers %r', + signal.receivers) + + self.paused[signal] = signal.receivers + signal.receivers = [] + + def __exit__(self, exc_type, exc_value, traceback): + for signal, receivers in self.paused.items(): + logger.debug('mute_signals: Restoring signal handlers %r', + receivers) + + signal.receivers = receivers + self.paused = {} + + def __call__(self, callable_obj): + if isinstance(callable_obj, base.FactoryMetaClass): + # Retrieve __func__, the *actual* callable object. + generate_method = callable_obj._generate.__func__ + + @classmethod + @functools.wraps(generate_method) + def wrapped_generate(*args, **kwargs): + with self: + return generate_method(*args, **kwargs) + + callable_obj._generate = wrapped_generate + return callable_obj + + else: + @functools.wraps(callable_obj) + def wrapper(*args, **kwargs): + with self: + return callable_obj(*args, **kwargs) + return wrapper + diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 34949c5..94599b7 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -107,18 +107,19 @@ class FuzzyChoice(BaseFuzzyAttribute): class FuzzyInteger(BaseFuzzyAttribute): """Random integer within a given range.""" - def __init__(self, low, high=None, **kwargs): + def __init__(self, low, high=None, step=1, **kwargs): if high is None: high = low low = 0 self.low = low self.high = high + self.step = step super(FuzzyInteger, self).__init__(**kwargs) def fuzz(self): - return random.randint(self.low, self.high) + return random.randrange(self.low, self.high + 1, self.step) class FuzzyDecimal(BaseFuzzyAttribute): @@ -140,6 +141,23 @@ class FuzzyDecimal(BaseFuzzyAttribute): return base.quantize(decimal.Decimal(10) ** -self.precision) +class FuzzyFloat(BaseFuzzyAttribute): + """Random float 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(FuzzyFloat, self).__init__(**kwargs) + + def fuzz(self): + return random.uniform(self.low, self.high) + + class FuzzyDate(BaseFuzzyAttribute): """Random date within a given date range.""" diff --git a/factory/helpers.py b/factory/helpers.py index 37b41bf..19431df 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -28,6 +28,7 @@ import logging from . import base from . import declarations +from . import django @contextlib.contextmanager @@ -49,7 +50,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..5541043 100644 --- a/factory/mogo.py +++ b/factory/mogo.py @@ -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.new(*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.new(*args, **kwargs) instance.save() return instance diff --git a/factory/mongoengine.py b/factory/mongoengine.py index 462f5f2..e3ab99c 100644 --- a/factory/mongoengine.py +++ b/factory/mongoengine.py @@ -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 |