diff options
Diffstat (limited to 'factory')
-rw-r--r-- | factory/__init__.py | 56 | ||||
-rw-r--r-- | factory/base.py | 636 | ||||
-rw-r--r-- | factory/compat.py | 35 | ||||
-rw-r--r-- | factory/containers.py | 114 | ||||
-rw-r--r-- | factory/declarations.py | 344 | ||||
-rw-r--r-- | factory/fuzzy.py | 86 | ||||
-rw-r--r-- | factory/helpers.py | 123 | ||||
-rw-r--r-- | factory/utils.py | 96 |
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) + |