summaryrefslogtreecommitdiff
path: root/factory
diff options
context:
space:
mode:
authorMark Sandstrom <mark@deliciouslynerdy.com>2010-08-22 11:33:45 -0700
committerMark Sandstrom <mark@deliciouslynerdy.com>2010-08-22 11:33:45 -0700
commit0431afa53f064529dd0d018f3c67f254352b66e7 (patch)
treede33558b2d231e6dfb9e10a56ac6287ab2cec090 /factory
downloadfactory-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__.py40
-rw-r--r--factory/base.py244
-rw-r--r--factory/containers.py47
-rw-r--r--factory/declarations.py76
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