diff options
author | Mark Sandstrom <mark@deliciouslynerdy.com> | 2010-08-22 11:33:45 -0700 |
---|---|---|
committer | Mark Sandstrom <mark@deliciouslynerdy.com> | 2010-08-22 11:33:45 -0700 |
commit | 0431afa53f064529dd0d018f3c67f254352b66e7 (patch) | |
tree | de33558b2d231e6dfb9e10a56ac6287ab2cec090 /factory | |
download | factory-boy-0431afa53f064529dd0d018f3c67f254352b66e7.tar factory-boy-0431afa53f064529dd0d018f3c67f254352b66e7.tar.gz |
factory_boy: a test fixtures replacement based on thoughtbot's factory_girl for Ruby
Diffstat (limited to 'factory')
-rw-r--r-- | factory/__init__.py | 40 | ||||
-rw-r--r-- | factory/base.py | 244 | ||||
-rw-r--r-- | factory/containers.py | 47 | ||||
-rw-r--r-- | factory/declarations.py | 76 |
4 files changed, 407 insertions, 0 deletions
diff --git a/factory/__init__.py b/factory/__init__.py new file mode 100644 index 0000000..650f572 --- /dev/null +++ b/factory/__init__.py @@ -0,0 +1,40 @@ +# Copyright (c) 2010 Mark Sandstrom +# +# 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. + +__version__ = '1.0.0' # Remember to change in setup.py as well! +__author__ = 'Mark Sandstrom <mark@deliciouslynerdy.com>' + +from base import ( + Factory, + StubFactory, + BUILD_STRATEGY, + CREATE_STRATEGY, + STUB_STRATEGY +) + +from declarations import ( + LazyAttribute, + Sequence, + LazyAttributeSequence, + lazy_attribute, + sequence, + lazy_attribute_sequence +) + diff --git a/factory/base.py b/factory/base.py new file mode 100644 index 0000000..5f712a2 --- /dev/null +++ b/factory/base.py @@ -0,0 +1,244 @@ +# Copyright (c) 2010 Mark Sandstrom +# +# 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. + +import re +import sys + +from containers import ObjectParamsWrapper, StubObject +from declarations import OrderedDeclaration + +# 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) + +# Special declarations + +FACTORY_CLASS_DECLARATION = 'FACTORY_FOR' + +# Factory class attributes + +CLASS_ATTRIBUTE_ORDERED_DECLARATIONS = '_ordered_declarations' +CLASS_ATTRIBUTE_UNORDERED_DECLARATIONS = '_unordered_declarations' +CLASS_ATTRIBUTE_ASSOCIATED_CLASS = '_associated_class' + +# Factory metaclasses + +def get_factory_base(bases): + parents = [b for b in bases if isinstance(b, BaseFactoryMetaClass)] + if not parents: + return None + if len(parents) > 1: + raise RuntimeError('You can only inherit from one Factory') + return parents[0] + +class BaseFactoryMetaClass(type): + '''Factory metaclass for handling ordered declarations.''' + + def __call__(cls, **kwargs): + '''Create an associated class instance using the default build strategy. Never create a Factory instance.''' + + if cls.default_strategy == BUILD_STRATEGY: + return cls.build(**kwargs) + elif cls.default_strategy == CREATE_STRATEGY: + return cls.create(**kwargs) + elif cls.default_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, dict, extra_dict={}): + '''Record attributes (unordered declarations) and ordered declarations for construction of + an associated class instance at a later time.''' + + base = get_factory_base(bases) + if not base: + # If this isn't a subclass of Factory, don't do anything special. + return super(BaseFactoryMetaClass, cls).__new__(cls, class_name, bases, dict) + + ordered_declarations = getattr(base, CLASS_ATTRIBUTE_ORDERED_DECLARATIONS, []) + unordered_declarations = getattr(base, CLASS_ATTRIBUTE_UNORDERED_DECLARATIONS, []) + + for name in list(dict): + if isinstance(dict[name], OrderedDeclaration): + ordered_declarations = [(_name, declaration) for (_name, declaration) in ordered_declarations if _name != name] + ordered_declarations.append((name, dict[name])) + del dict[name] + elif not name.startswith('__'): + unordered_declarations = [(_name, value) for (_name, value) in unordered_declarations if _name != name] + unordered_declarations.append((name, dict[name])) + del dict[name] + + ordered_declarations.sort(key=lambda d: d[1].order) + + dict[CLASS_ATTRIBUTE_ORDERED_DECLARATIONS] = ordered_declarations + dict[CLASS_ATTRIBUTE_UNORDERED_DECLARATIONS] = unordered_declarations + + for name, value in extra_dict.iteritems(): + dict[name] = value + + return super(BaseFactoryMetaClass, cls).__new__(cls, class_name, bases, dict) + +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}.''' + + def __new__(cls, class_name, bases, dict): + '''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.''' + + base = get_factory_base(bases) + if not base: + # If this isn't a subclass of Factory, don't do anything special. + return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, dict) + + inherited_associated_class = getattr(base, CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None) + own_associated_class = None + used_auto_discovery = False + + if FACTORY_CLASS_DECLARATION in dict: + own_associated_class = dict[FACTORY_CLASS_DECLARATION] + del dict[FACTORY_CLASS_DECLARATION] + else: + factory_module = sys.modules[dict['__module__']] + match = re.match(r'^(\w+)Factory$', class_name) + if match: + used_auto_discovery = True + associated_class_name = match.group(1) + try: + own_associated_class = getattr(factory_module, associated_class_name) + except AttributeError: + pass + + if own_associated_class != None and inherited_associated_class != None and own_associated_class != inherited_associated_class: + format = 'These factories are for conflicting classes: {0} and {1}' + raise Factory.AssociatedClassError(format.format(inherited_associated_class, own_associated_class)) + elif inherited_associated_class != None: + own_associated_class = inherited_associated_class + + if not own_associated_class and used_auto_discovery: + format_args = FACTORY_CLASS_DECLARATION, associated_class_name, class_name, factory_module + raise Factory.AssociatedClassError(FactoryMetaClass.ERROR_MESSAGE_AUTODISCOVERY.format(*format_args)) + elif not own_associated_class: + raise Factory.AssociatedClassError(FactoryMetaClass.ERROR_MESSAGE.format(FACTORY_CLASS_DECLARATION)) + + extra_dict = {CLASS_ATTRIBUTE_ASSOCIATED_CLASS: own_associated_class} + return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, dict, extra_dict=extra_dict) + +# Factory base classes + +class BaseFactory(object): + '''Factory base support for sequences, attributes and stubs.''' + + class UnknownStrategy(RuntimeError): pass + class UnsupportedStrategy(RuntimeError): pass + + def __new__(cls, *args, **kwargs): + raise RuntimeError('You cannot instantiate BaseFactory') + + _next_sequence = 0 + + @classmethod + def _generate_next_sequence(cls): + next_sequence = cls._next_sequence + cls._next_sequence += 1 + return next_sequence + + @classmethod + def attributes(cls, **kwargs): + attributes = {} + cls.sequence = cls._generate_next_sequence() + + for name, value in getattr(cls, CLASS_ATTRIBUTE_UNORDERED_DECLARATIONS): + if name in kwargs: + attributes[name] = kwargs[name] + del kwargs[name] + else: + attributes[name] = value + + for name, ordered_declaration in getattr(cls, CLASS_ATTRIBUTE_ORDERED_DECLARATIONS): + if name in kwargs: + attributes[name] = kwargs[name] + del kwargs[name] + else: + a = ObjectParamsWrapper(attributes) + attributes[name] = ordered_declaration.evaluate(cls, a) + + for name in kwargs: + attributes[name] = kwargs[name] + + return attributes + + @classmethod + def build(cls, **kwargs): + raise cls.UnsupportedStrategy() + + @classmethod + def create(cls, **kwargs): + raise cls.UnsupportedStrategy() + + @classmethod + def stub(cls, **kwargs): + stub_object = StubObject() + for name, value in cls.attributes(**kwargs).iteritems(): + setattr(stub_object, name, value) + return stub_object + +class StubFactory(BaseFactory): + __metaclass__ = BaseFactoryMetaClass + + default_strategy = STUB_STRATEGY + +class Factory(BaseFactory): + '''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 + + _creation_function = (DJANGO_CREATION,) # Using a tuple to keep the creation function from turning into an instance method + @classmethod + def set_creation_function(cls, creation_function): + cls._creation_function = (creation_function,) + @classmethod + def get_creation_function(cls): + return cls._creation_function[0] + + @classmethod + def build(cls, **kwargs): + return getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS)(**cls.attributes(**kwargs)) + + @classmethod + def create(cls, **kwargs): + return cls.get_creation_function()(getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS), **cls.attributes(**kwargs)) diff --git a/factory/containers.py b/factory/containers.py new file mode 100644 index 0000000..08f0450 --- /dev/null +++ b/factory/containers.py @@ -0,0 +1,47 @@ +# Copyright (c) 2010 Mark Sandstrom +# +# 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. + +class ObjectParamsWrapper(object): + '''A generic container that allows for getting but not setting of attributes. + + Attributes are set at initialization time.''' + + initialized = False + + def __init__(self, dict): + self.dict = dict + self.initialized = True + + def __setattr__(self, name, value): + if not self.initialized: + return super(ObjectParamsWrapper, self).__setattr__(name, value) + else: + raise AttributeError('Setting of object attributes is not allowed') + + def __getattr__(self, name): + try: + return self.dict[name] + except KeyError: + raise AttributeError("The param '{0}' does not exist. Perhaps your declarations are out of order?".format(name)) + +class StubObject(object): + '''A generic container.''' + + pass
\ No newline at end of file diff --git a/factory/declarations.py b/factory/declarations.py new file mode 100644 index 0000000..37c8cbd --- /dev/null +++ b/factory/declarations.py @@ -0,0 +1,76 @@ +# Copyright (c) 2010 Mark Sandstrom +# +# 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. + +class OrderedDeclaration(object): + '''A factory declaration. + + Ordered declarations keep track of the order in which they're defined so that later declarations + can refer to attributes created by earlier declarations when the declarations are evaluated.''' + _next_order = 0 + + def __init__(self): + self.order = self.next_order() + + @classmethod + def next_order(cls): + next_order = cls._next_order + cls._next_order += 1 + return next_order + + def evaluate(self, factory, attributes): + '''Evaluate this declaration. + + Args: + factory: The factory this declaration was defined in. + attributes: The attributes created by the unordered and ordered declarations up to this point.''' + + raise NotImplementedError('This is an abstract method') + +class LazyAttribute(OrderedDeclaration): + def __init__(self, function): + super(LazyAttribute, self).__init__() + self.function = function + + def evaluate(self, factory, attributes): + return self.function(attributes) + +class Sequence(OrderedDeclaration): + def __init__(self, function, type=str): + super(Sequence, self).__init__() + self.function = function + self.type = type + + def evaluate(self, factory, attributes): + return self.function(self.type(factory.sequence)) + +class LazyAttributeSequence(Sequence): + def evaluate(self, factory, attributes): + return self.function(attributes, self.type(factory.sequence)) + +# Decorators... in case lambdas don't cut it + +def lazy_attribute(func): + return LazyAttribute(func) + +def sequence(func): + return Sequence(func) + +def lazy_attribute_sequence(func): + return LazyAttributeSequence(func)
\ No newline at end of file |