Commits

Chad Dombrova committed 86844c3 Merge

Merge with orms_sqlalchemy

Comments (0)

Files changed (21)

denormalize/models.py

-from django.db import models
 from .orms.base import ModelInspector, inspector, Skip
 import logging
 
         return self.dump(self.model.get_native_object(root_pk))
 
     def dump(self, root_obj):
-        if not self.model.is_compatible(root_obj):
+        if not self.model.is_compatible_instance(root_obj):
             raise ValueError("root_obj is not an instance of self.model")
         return self.dump_obj(self.model, root_obj, path=[])
 
         if not isinstance(model, ModelInspector):
             raise ValueError("%r is not an instance of ModelInspector" % model)
 
-        if not model.is_compatible(obj):
+        if not model.is_compatible_instance(obj):
             raise ValueError("%r is not an instance of %s" %
                              (obj, model._model))
 
         # FIXME: obj.pk is django-specific
-        log.debug("dump_obj for %s %s, path '%s'",
-                  model.name, obj.pk, '__'.join(path))
+        # log.debug("dump_obj for %s %s, path '%s'",
+        #           model.name, obj.pk, '__'.join(path))
 
         # Filter the fields based on user preferences
         excluded = self._excluded_for_path(path)
 
         # print " " * len(path), model.name, path
-        # print " " * len(path), "rawfields", [f.name for f in meta.fields]
         # print " " * len(path), "fields   ", list(model.iter_fields(obj))
-        # print " " * len(path), "fkeys    ", list(model.iter_foreign_keys(obj))
-        # print " " * len(path), "one2one  ", list(model.iter_relations(obj, ['one-to-one']))
-        # print " " * len(path), "many2one ", list(model.iter_relations(obj, ['many-to-one']))
-        # print " " * len(path), "one2many ", list(model.iter_relations(obj, ['one-to-many']))
-        # print " " * len(path), "many2many", list(model.iter_relations(obj, ['many-to-many']))
+        # print " " * len(path), "has  one ", list(model.iter_relations(obj, 'has one'))
+        # print " " * len(path), "has many ", list(model.iter_relations(obj, 'has many'))
         # print
 
         # Build the output dictionary
         data = {}
 
-        # Class name
-        if self.add_model_name:
-            # FIXME: app_label is django-specific
-            data['_model'] = model._model._meta.app_label + ':' + model.name
+        # # Class name
+        # if self.add_model_name:
+        #     # FIXME: app_label is django-specific
+        #     data['_model'] = model._model._meta.app_label + ':' + model.name
 
         fields_to_follow = self._fk_fields_to_follow(path)
         # Follow allowed relations
                     pass
 
         # *-to-one relationships
-        for relname, field_id in model.iter_relations(obj, ['one-to-one',
-                                                            'many-to-one']):
+        for relname, field_id in model.iter_relations(obj, 'has one'):
             if relname not in excluded:
                 if relname in fields_to_follow:
                     try:
                         pass
 
         # *-to-many relationships
-        for relname, _ in model.iter_relations(obj, ['one-to-many',
-                                                     'many-to-many']):
+        for relname, _ in model.iter_relations(obj, 'has many'):
             if relname in included_relations:
                 try:
                     values = model.get_value(obj, relname)

denormalize/orms/base.py

 #     pass
 
 class ModelInspector(object):
+    _inspectors = []
+
     def __init__(self, model):
         self._model = model
 
     def name(self):
         return self._model.__name__
 
-    def is_compatible(self, obj):
+    def is_compatible_instance(self, obj):
         return isinstance(obj, self._model)
 
+    @classmethod
+    def register(cls):
+        cls._inspectors.append(cls)
+
     # TODO: move over base methods from DjangoModelInspector when locked down
 
 def inspector(model):
-    # FIXME: register inspectors/orms
-    from .django import DjangoModelInspector
+
     if isinstance(model, ModelInspector):
         return model
-    return DjangoModelInspector(model)
+
+    # FIXME: more automated registration of inspectors/orms
+    try:
+        from . import django
+    except:
+        pass
+    try:
+        from . import sqlalchemy
+    except:
+        pass
+
+    for insp in ModelInspector._inspectors:
+        if insp.is_compatible_class(model):
+            return insp(model)

denormalize/orms/django.py

     def table_name(self):
         self._model._meta.db_table
 
+    @staticmethod
+    def is_compatible_class(model_class):
+        return issubclass(model_class, models.Model)
+
     def get_native_object(self, primary_key):
         return self._model.objects.get(pk=primary_key)
 
                 field_id = '{0}_id'.format(name)
                 yield name, field_id, field
 
-    def iter_foreign_keys(self, obj=None):
-        for name, field_id, field in self._iter_foreign_keys(obj):
-            yield name, field_id
+    # def iter_foreign_keys(self, obj=None):
+    #     for name, field_id, field in self._iter_foreign_keys(obj):
+    #         yield name, field_id
 
-    def _iter_relations(self, obj, cardinality, source):
-        # FIXME: split cardinality into 2 args? local_has_many, remote_has_many?
-        if isinstance(cardinality, basestring):
-            cardinality = [cardinality]
-        for c in cardinality:
-            assert c in ('one-to-one', 'one-to-many',
-                                   'many-to-one', 'many-to-many')
-        assert source in ('local', 'remote', 'any')
+    # def _iter_relations(self, obj, cardinality, source):
+    #     # FIXME: split cardinality into 2 args? local_has_many, remote_has_many?
+    #     if isinstance(cardinality, basestring):
+    #         cardinality = [cardinality]
+    #     for c in cardinality:
+    #         assert c in ('one-to-one', 'one-to-many',
+    #                      'many-to-one', 'many-to-many')
+    #     assert source in ('local', 'remote', 'any')
+
+    #     if obj is not None:
+    #         meta = obj._meta
+    #     else:
+    #         meta = self._model._meta
+
+    #     # FIXME: look into combining these loops
+    #     if 'one-to-one' in cardinality:
+    #         if source in ('any', 'remote'):
+    #             for rel, model in meta.get_all_related_objects_with_model():
+    #                 if isinstance(rel.field, models.OneToOneField):
+    #                     yield rel.get_accessor_name(), None, rel
+
+    #         if source in ('any', 'local'):
+    #             for rel, field_id, field in self._iter_foreign_keys(obj):
+    #                 if isinstance(field, models.OneToOneField):
+    #                     yield rel, field_id, field
+
+    #     if 'many-to-one' in cardinality:
+    #         if source in ('any', 'local'):
+    #             for rel, field_id, field in self._iter_foreign_keys(obj):
+    #                 if not isinstance(field, models.OneToOneField):
+    #                     yield rel, field_id, field
+
+    #     if 'one-to-many' in cardinality:
+    #         if source in ('any', 'remote'):
+    #             for rel, model in meta.get_all_related_objects_with_model():
+    #                 if not isinstance(rel.field, models.OneToOneField):
+    #                     yield rel.get_accessor_name(), None, rel
+
+    #     if 'many-to-many' in cardinality:
+    #         if source in ('any', 'remote'):
+    #             for rel, model in meta.get_all_related_m2m_objects_with_model():
+    #                 yield rel.get_accessor_name(), None, rel
+    #         if source in ('any', 'local'):
+    #             for rel, model in meta.get_m2m_with_model():
+    #                 yield rel.name, None, rel
+
+    # def iter_relations(self, obj, cardinality, source='any'):
+    #     for relname, field_id, _ in self._iter_relations(obj, cardinality,
+    #                                                      source):
+    #         yield relname, field_id
+
+    def _iter_relations(self, obj, cardinality):
+        assert cardinality in ('has one', 'has many', 'any')
 
         if obj is not None:
             meta = obj._meta
             meta = self._model._meta
 
         # FIXME: look into combining these loops
-        if 'one-to-one' in cardinality:
-            if source in ('any', 'remote'):
-                for rel, model in meta.get_all_related_objects_with_model():
-                    if isinstance(rel.field, models.OneToOneField):
-                        yield rel.get_accessor_name(), None, rel
+        if cardinality in ('has one', 'any'):
+            for rel, model in meta.get_all_related_objects_with_model():
+                if isinstance(rel.field, models.OneToOneField):
+                    yield rel.get_accessor_name(), None, rel
 
-            if source in ('any', 'local'):
-                for rel, field_id, field in self._iter_foreign_keys(obj):
-                    if isinstance(field, models.OneToOneField):
-                        yield rel, field_id, field
+            for rel, field_id, field in self._iter_foreign_keys(obj):
+                yield rel, field_id, field
 
-        if 'many-to-one' in cardinality:
-            if source in ('any', 'local'):
-                for rel, field_id, field in self._iter_foreign_keys(obj):
-                    if not isinstance(field, models.OneToOneField):
-                        yield rel, field_id, field
+        if cardinality in ('has many', 'any'):
+            for rel, model in meta.get_all_related_objects_with_model():
+                if not isinstance(rel.field, models.OneToOneField):
+                    yield rel.get_accessor_name(), None, rel
 
-        if 'one-to-many' in cardinality:
-            if source in ('any', 'remote'):
-                for rel, model in meta.get_all_related_objects_with_model():
-                    if not isinstance(rel.field, models.OneToOneField):
-                        yield rel.get_accessor_name(), None, rel
+            for rel, model in meta.get_all_related_m2m_objects_with_model():
+                yield rel.get_accessor_name(), None, rel
 
-        if 'many-to-many' in cardinality:
-            if source in ('any', 'remote'):
-                for rel, model in meta.get_all_related_m2m_objects_with_model():
-                    yield rel.get_accessor_name(), None, rel
-            if source in ('any', 'local'):
-                for rel, model in meta.get_m2m_with_model():
-                    yield rel.name, None, rel
+            for rel, model in meta.get_m2m_with_model():
+                yield rel.name, None, rel
 
-    def iter_relations(self, obj, cardinality, source='any'):
-        for relname, field_id, _ in self._iter_relations(obj, cardinality,
-                                                         source):
+    def iter_relations(self, obj, cardinality='any'):
+        for relname, field_id, _ in self._iter_relations(obj, cardinality):
             yield relname, field_id
 
     def get_value(self, obj, accessor):
             through_model = info['through']._model
             signals.m2m_changed.connect(m2m_changed, sender=through_model)
 
+DjangoModelInspector.register()

denormalize/orms/sqlalchemy.py

+from __future__ import absolute_import
+from sqlalchemy.orm import class_mapper
+import sqlalchemy.orm.properties as sqlprops
+import sqlalchemy.sql.expression as sqlexpr
+import sqlalchemy.schema as sqlschema
+from sqlalchemy import types as sqltypes
+from sqlalchemy.orm.exc import UnmappedClassError
+from sqlalchemy.orm.collections import InstrumentedList
+
+from .base import Skip, ModelInspector, ModelInstancePair
+import logging
+
+log = logging.getLogger(__name__)
+
+def _is_model_class(cls):
+    try:
+        class_mapper(cls)
+        return True
+    except UnmappedClassError:
+        return False
+
+class SqlAlchemyModelInspector(ModelInspector):
+    def __init__(self, model):
+        super(SqlAlchemyModelInspector, self).__init__(model)
+        self._mapper = class_mapper(model)
+        self._field_info_cache = None
+        self._relation_info_cache = None
+
+    @property
+    def table_name(self):
+        self._mapper.table.name
+
+    @staticmethod
+    def is_compatible_class(model_class):
+        return _is_model_class(model_class)
+
+    def get_native_object(self, primary_key):
+        return self._model.objects.get(pk=primary_key)
+
+    def get_field_info(self, accessor):
+        if accessor in self._field_info_cache:
+            return self._field_info_cache[accessor]
+
+        # Most of the time these are equal
+        fieldname = accessor
+        info = {}
+
+        # Get field info
+        # TODO: show list of valid names if the field is invalid
+        try:
+            field, field_model, direct, m2m = \
+                self._model._meta.get_field_by_name(fieldname)
+        except models.FieldDoesNotExist:
+            if accessor.endswith('_set'):
+                # If no related_name is specified, the field name will
+                # be 'foo', while the queryset is accessible through
+                # 'foo_set'. If the related_name has been set, these
+                # will be the same.
+                fieldname = accessor[:-4] # strip '_set'
+                field, field_model, direct, m2m = \
+                    self._model._meta.get_field_by_name(fieldname)
+            else:
+                raise
+
+        # We want to get the next model in our iteration
+        if direct and not m2m:
+            # (<related.ForeignKey: publisher>, None, True, False)
+            model = field.related.parent_model
+        elif direct and m2m:
+            # (<related.ManyToManyField: authors>, None, True, True)
+            # (<related.ManyToManyField: tags>, None, True, True)
+            model = field.rel.to
+            info['through'] = self.__class__(field.rel.through)
+        elif not direct and not m2m:
+            # (<RelatedObject: tests:extrabookinfo related to book>, None, False, False)
+            # (<RelatedObject: tests:chapter related to book>, None, False, False)
+            # (<RelatedObject: tests:publisherlink related to publisher>, None, False, False)
+            model = field.model
+        elif not direct and m2m:
+            # (<RelatedObject: tests:category related to books>, None, False, True)
+            model = field.model
+            info['through'] = self.__class__(field.field.rel.through)
+        else:
+            # Cannot happen, we covered all four cases
+            raise AssertionError("Impossible")
+
+        info['model'] = self.__class__(model)
+        info['m2m'] = m2m
+        info['direct'] = direct
+        self._field_info_cache[accessor] = fieldname, info
+        return fieldname, info
+
+    # -- fields
+
+    def _get_basic_relation(self, column):
+        """Determine if the column is a foreign key used in a basic one-to-one
+        relationship on this model.
+
+        It is often desirable to filter these columns in favor of thier
+        relationships since the id will be contained within the embedded
+        sub-document.
+
+        For example:
+
+            class Company(Base):
+                owner_id = sqlalchemy.Column(
+                    sqlalchemy.Integer,
+                    sqlalchemy.ForeignKey('user.id')
+
+                owner = sqlalchemy.orm.relationship(
+                    'User',
+                    # foreign_keys=[owner_id], # produces same as next line
+                    primaryjoin=owner_id==User.id)
+
+        """
+        if isinstance(column, sqlprops.ColumnProperty):
+            column = column.columns[0]
+
+        if column.foreign_keys:
+            if len(column.foreign_keys) != 1:
+                return
+            foreign_key = list(column.foreign_keys)[0]
+            match = [(column, foreign_key.column),
+                     (foreign_key.column, column)]
+            for relation in self._mapper.relationships:
+                join = relation.primaryjoin
+                if (isinstance(join, sqlexpr.BinaryExpression)
+                        and relation.secondaryjoin is None
+                        and (join.left, join.right) in match):
+                    return relation
+        return
+
+    def _fill_field_info(self):
+        if self._field_info_cache is not None:
+            return
+        self._field_info_cache = {}
+        self._relation_info_cache = {}
+        for name, field in self._iter_fields():
+            relation = self._get_basic_relation(field)
+            if relation is not None:
+                self._field_info_cache[name] = relation
+                self._relation_info_cache[relation.key] = name
+
+    def _iter_fields(self, obj=None):
+        for field in self._mapper.column_attrs:
+            yield field.key, field
+
+    def iter_fields(self, obj=None):
+        self._fill_field_info()
+
+        for name, field in self._iter_fields(obj):
+            if name not in self._field_info_cache:
+                yield name
+
+    # -- relationships
+
+    def _iter_relations(self, obj, cardinality):
+        assert cardinality in ('has one', 'has many', 'any')
+
+        self._fill_field_info()
+
+        for rel in self._mapper.relationships:
+            cardinal = "has many" if rel.uselist else "has one"
+            if cardinality in ('any', cardinal):
+                fk_field = self._relation_info_cache.get(rel.key)
+                yield rel.key, fk_field, rel
+
+    def iter_relations(self, obj, cardinality='any'):
+        for relname, field_id, _ in self._iter_relations(obj, cardinality):
+            yield relname, field_id
+
+    def get_value(self, obj, accessor):
+        value = getattr(obj, accessor)
+
+        if _is_model_class(value.__class__):
+            model = self.__class__(value.__class__)
+            return ModelInstancePair(model, value)
+        elif isinstance(value, InstrumentedList):
+            result = []
+            for res in value:
+                model = self.__class__(res.__class__)
+                result.append(ModelInstancePair(model, res))
+            return result
+        else:
+            return value
+
+SqlAlchemyModelInspector.register()

denormalize/tests/__init__.py

-from .test_collections import CollectionTest
-from .test_backends import *
-from .test_django import *
+# from .test_collections import CollectionTest
+# from .test_backends import *
+# from .test_django import *

denormalize/tests/common.py

-from django import test
-
+from ..models import DocumentCollection
+import os
+from pprint import pprint
 import logging
 log = logging.getLogger(__name__)
 
-class ModelTestCase(test.TestCase):
-    # Source:
-    # http://stackoverflow.com/questions/502916/django-how-to-create-a-model
-    test_models_app = 'denormalize.tests'
-    _test_models_initiated = False
 
+# TODO: check if the SQL queries are efficient (all data should be fetched in one query)
+
+class BookCollection(DocumentCollection):
+    model = None
+    name = "books"
+    select_related = ['publisher', 'extra_info']
+    prefetch_related = ['chapter_set', 'authors', 'tags', 'publisher__links',
+        'category_set', 'publisher__tags']
+    exclude = ['authors__email', 'summary']
+    add_model_name = True
+
+    def dump_obj(self, model, obj, path):
+        # Customization: for tags, use the name string as the representation
+        if model.name == 'Tag':
+            return obj.name
+        # Here only the url
+        if model.name == 'PublisherLink':
+            return obj.url
+        return super(BookCollection, self).dump_obj(model, obj, path)
+
+
+class CollectionTestMixin(object):
     @classmethod
-    def setUpClass(cls, *args, **kwargs):
-        if not cls._test_models_initiated:
-            cls.create_models_from_app(cls.test_models_app)
-            cls._test_models_initiated = True
-        super(ModelTestCase, cls).setUpClass(*args, **kwargs)
+    def setUpClass(cls):
+        # creates the tables:
+        super(CollectionTestMixin, cls).setUpClass()
+        cls.objs = cls.models.create_test_data()
 
-    @classmethod
-    def create_models_from_app(cls, app_name):
-        """
-        Manually create Models (used only for testing) from the specified string app name.
-        Models are loaded from the module "<app_name>.models"
-        """
-        from django.db import connection, DatabaseError
-        from django.db.models.loading import load_app
+        #book = Book.objects.get(title=u"Cooking for Geeks")
+        #m = book._meta
+        #import code; _d = dict(); _d.update(globals()); _d.update(locals()); code.interact(local=_d)
 
-        app = load_app(app_name)
-        from django.core.management import sql
-        from django.core.management.color import no_style
-        sql = sql.sql_create(app, no_style(), connection)
-        cursor = connection.cursor()
-        for statement in sql:
-            try:
-                cursor.execute(statement)
-            except DatabaseError, excn:
-                log.debug("DatabaseError in statement: %s",
-                    statement, exc_info=True)
-  
+    def test_dump(self):
+        bookcol = self.collection()
+        doc = bookcol.dump(self.objs['book'])
+
+        # Basic fields
+        self.assertTrue('id' in doc)
+        self.assertTrue('title' in doc)
+        self.assertFalse('summary' in doc, "The summary was not excluded")
+        self.assertTrue('year' in doc)
+        self.assertTrue('created' in doc)
+        self.assertEqual(doc['id'], self.objs['book'].id)
+
+        # Relational fields
+        self.assertTrue('publisher' in doc, doc)
+        self.assertEqual(doc['publisher']['name'], u"O'Reilly")
+        self.assertTrue('authors' in doc, doc)
+        self.assertEqual(len(doc['authors']), 1)
+        self.assertTrue('chapter_set' in doc, doc)
+        self.assertEqual(len(doc['chapter_set']), 7)
+
+        # Other expectations
+
+        # - Tags are pure strings
+        self.assertEqual(doc['tags'], [u'cooking', u'geeks', u'technology'])
+
+        # - ForeignKeys not explicitly followed are included as a pure id
+        chapter = doc['chapter_set'][0]
+        # TODO: we should probably omit it if it's the relation we just followed
+        self.assertTrue('book_id' in chapter,
+            "If a FK is not followed, its id will be included")
+
+        # - Author emails are excluded
+        author = doc['authors'][0]
+        self.assertFalse('email' in author, "The author email was not excluded")
+
+    def test_dump_collection(self):
+        bookcol = self.collection()
+        docs = list(bookcol.dump_collection())
+        self.assertEqual(len(docs), 2)
+        if 'pprint' in os.environ:
+            pprint(docs)
+
+    def test_get_related_models(self):
+        bookcol = self.collection()
+        deps = bookcol.get_related_models()
+        for rel in bookcol.select_related + bookcol.prefetch_related:
+            # Difference between the filter name and the path name
+            if rel == 'chapter_set':
+                rel = 'chapter'
+            elif rel == 'category_set':
+                rel = 'category'
+            self.assertTrue(rel in deps, "Relation {0} not found!".format(rel))
+        pprint(deps)
+        self.assertIs(deps['publisher']['model']._model, self.models.Publisher)
+        self.assertIs(deps['extra_info']['model']._model, self.models.ExtraBookInfo)
+        self.assertIs(deps['chapter']['model']._model, self.models.Chapter)
+        self.assertIs(deps['authors']['model']._model, self.models.Author)
+        self.assertIs(deps['tags']['model']._model, self.models.Tag)
+        self.assertIs(deps['publisher__links']['model']._model, self.models.PublisherLink)

denormalize/tests/models.py

-"""
-Very simple book model, for test purposes.
-
-A book has:
-
- * One or more authors, that can also be authors of other books (many-to-many)
- * One publisher (foreign key)
- * One or more chapters, that are always book specific (reverse foreign key)
-
-"""
-
-from django.db import models
-from django.utils.translation import ugettext_lazy as _
-
-class Tag(models.Model):
-    name = models.CharField(_("name"), max_length=40)
-
-    def __unicode__(self):
-        return self.name
-
-    class Meta:
-        verbose_name = 'tag'
-        verbose_name_plural = 'tags'
-
-
-class Author(models.Model):
-    name = models.CharField(_("name"), max_length=80)
-    email = models.EmailField(_("email"), blank=True)
-
-    def __unicode__(self):
-        return self.name
-
-    class Meta:
-        verbose_name = 'author'
-        verbose_name_plural = 'authors'
-
-
-class Publisher(models.Model):
-    name = models.CharField(_("name"), max_length=80)
-    email = models.EmailField(_("email"), blank=True)
-    tags = models.ManyToManyField(Tag)
-
-    def __unicode__(self):
-        return self.name
-
-    class Meta:
-        verbose_name = 'publisher'
-        verbose_name_plural = 'publishers'
-
-
-class PublisherLink(models.Model):
-    publisher = models.ForeignKey(Publisher, related_name='links')
-    url = models.URLField(_("url"))
-
-    def __unicode__(self):
-        return self.url
-
-    class Meta:
-        verbose_name = 'publisher link'
-        verbose_name_plural = 'publisher links'
-
-
-class Book(models.Model):
-    title = models.CharField(_("title"), max_length=80)
-    year = models.PositiveIntegerField(_("year"), null=True)
-    summary = models.TextField(_("summary"), blank=True)
-    authors = models.ManyToManyField(Author)
-    publisher = models.ForeignKey(Publisher)
-    tags = models.ManyToManyField(Tag)
-    created = models.DateTimeField(_('created on'), auto_now_add=True)
-
-    def __unicode__(self):
-        return 'Book %s' % (self.id,)
-
-    class Meta:
-        verbose_name = 'book'
-        verbose_name_plural = 'books'
-
-
-class Chapter(models.Model):
-    # FIXME: if related_name is not set, the right filter query is 'chapter'...
-    book = models.ForeignKey(Book)#, related_name='chapter_set')
-    title = models.CharField(_("title"), max_length=80)
-
-    def __unicode__(self):
-        return 'Chapter %s' % (self.id,)
-
-    class Meta:
-        verbose_name = 'chapter'
-        verbose_name_plural = 'chapters'
-
-
-class ExtraBookInfo(models.Model):
-    # This is for testing OneToOneField
-    book = models.OneToOneField(Book, related_name='extra_info')
-    isbn = models.CharField(_("isbn"), max_length=80, blank=True)
-
-    def __unicode__(self):
-        return 'ExtraBookInfo %s' % (self.id,)
-
-    class Meta:
-        verbose_name = 'extra book info'
-        verbose_name_plural = 'extra book info'
-
-
-class Category(models.Model):
-    # To create a reverse m2m relationship for testing
-    name = models.CharField(_("name"), max_length=80)
-    books = models.ManyToManyField(Book)
-
-    def __unicode__(self):
-        return self.name
-
-    class Meta:
-        verbose_name = 'category'
-        verbose_name_plural = 'categories'
-
-
-
-def create_test_books():
-    jeff = Author.objects.create(name=u"Jeff Potter")
-    oreilly = Publisher.objects.create(
-        name=u"O'Reilly",
-        email='orders@oreilly.com',
-    )
-    PublisherLink.objects.create(publisher=oreilly, url='http://oreilly.com/')
-    cooking_for_geeks = Book.objects.create(
-        title=u"Cooking for Geeks",
-        publisher=oreilly,
-        year=2010
-    )
-    cooking_for_geeks.authors = [jeff]
-    cooking_for_geeks.tags = [
-        Tag.objects.get_or_create(name="cooking")[0],
-        Tag.objects.get_or_create(name="geeks")[0],
-        Tag.objects.get_or_create(name="technology")[0],
-    ]
-    ExtraBookInfo.objects.create(book=cooking_for_geeks, isbn='978-0-596-80588-3')
-    chapters = [
-        u"Hello, Kitchen!",
-        u"Initializing the Kitchen",
-        u"Choosing Your Inputs: Flavors and Ingredients",
-        u"Time and Temperature: Cooking's Primary Variables",
-        u"Air: Baking's Key Variable",
-        u"Playing with Chemicals",
-        u"Fun with Hardware"
-    ]
-    for title in chapters:
-        Chapter.objects.create(book=cooking_for_geeks, title=title)
-    oreilly.tags = [Tag.objects.get_or_create(name="technology")[0]]
-
-    kristina = Author.objects.create(name=u"Kristina Chodorow")
-    michael = Author.objects.create(name=u"Michael Dirolf")
-    mongodb = Book.objects.create(
-        title=u"MongoDB: The Definitive Guide",
-        publisher=oreilly,
-        year=2010
-    )
-    mongodb.authors = [kristina, michael]
-    mongodb.tags = [
-        Tag.objects.get_or_create(name="mongodb")[0],
-        Tag.objects.get_or_create(name="databases")[0],
-        Tag.objects.get_or_create(name="technology")[0],
-    ]
-    ExtraBookInfo.objects.create(book=mongodb, isbn='978-1-4493-4468-9')
-    chapters = [u"Introduction", u"Getting Started", u"Querying"]
-    for title in chapters:
-        Chapter.objects.create(book=mongodb, title=title)
-
-    tech = Category.objects.create(name=u"Technology")
-    tech.books.add(cooking_for_geeks)
-    tech.books.add(mongodb)
-
-################################################################
-# To test some django reverse behavior
-
-class A(models.Model):
-    x = models.CharField(max_length=80)
-
-class B(models.Model):
-    a = models.ForeignKey(A)
-    x = models.CharField(max_length=80)
-
-class CBase(models.Model):
-    a = models.ForeignKey(A)
-    class Meta:
-        abstract = True
-
-class C(CBase):
-    x = models.CharField(max_length=80)
-
-

denormalize/tests/test_backends.py

 from ..backend.locmem import LocMemBackend
 from ..context import delay_sync, sync_together
 
-from .common import ModelTestCase
-from .models import *
-from .test_collections import BookCollection
-
+from .test_django import models
+from .test_django.common import ModelTestCase
 
 class BackendTest(ModelTestCase):
+    models = models
 
     SUPPORTS_SYNC_COLLECTION = True
 
         return LocMemBackend()
 
     def test_dump(self):
-        bookcol = BookCollection()
+        bookcol = self.collection()
         backend = self._create_backend()
         backend.register(bookcol)
         with sync_together():
-            create_test_books()
+            models.create_test_data()
 
         # Test data
         doc = backend.get_doc(bookcol, 1)
         self.assertTrue('tags' in doc, doc)
 
         # Change a one to many link
-        chapter = Chapter.objects.get(id=1)
+        chapter = models.Chapter.objects.get(id=1)
         chapter.title += u'!!!'
         chapter.save()
         doc = backend.get_doc(bookcol, 1)
         self.assertTrue(chapter['title'].endswith('!!!'), doc)
 
         # Change something (m2m)
-        author = Author.objects.get(id=1)
+        author = models.Author.objects.get(id=1)
         author.name = 'Another Name'
         author.email = 'foo@example.com'
         author.save()
         self.assertEqual(doc['authors'][0]['name'], author.name)
 
         # Change something that's shared (m2m)
-        tag = Tag.objects.get(name="technology")
+        tag = models.Tag.objects.get(name="technology")
         tag.name = "tech"
         tag.save()
         doc = backend.get_doc(bookcol, 1)
         self.assertTrue('tech' in doc['tags'])
 
         # Add a chapter to a book
-        book1 = Book.objects.get(id=1)
-        chapter = Chapter.objects.create(book=book1, title="Conclusion")
+        book1 = models.Book.objects.get(id=1)
+        chapter = models.Chapter.objects.create(book=book1, title="Conclusion")
         doc = backend.get_doc(bookcol, 1)
         self.assertTrue("Conclusion" in (x['title'] for x in doc['chapter_set']))
 
         # Move a chapter to another book (FK change!)
-        book2 = Book.objects.get(id=2)
+        book2 = models.Book.objects.get(id=2)
         chapter.book = book2
         chapter.save()
         doc = backend.get_doc(bookcol, 1)
         self.assertFalse("Conclusion" in (x['title'] for x in doc['chapter_set']))
 
         # Add a tag (m2m updated!!!) FIXME: not supported yet
-        tag = Tag.objects.create(name="foo")
+        tag = models.Tag.objects.create(name="foo")
         book2.tags = []
         book2.tags.add(tag)
         # Remove the tag from the book from the other side (reverse=True)
-        tag = Tag.objects.get(id=tag.id)
+        tag = models.Tag.objects.get(id=tag.id)
         tag.book_set.remove(book2)
         tag.book_set.add(book2)
         # 'clear' action
 
             # Next, with dirty records
             def inject_dirty():
-                newbook = Book.objects.create(
+                newbook = models.Book.objects.create(
                     title="Some title", publisher=book2.publisher)
                 self.assertTrue(newbook.id in backend._dirty['books'])
                 backend._dirty['books'].add(1)

denormalize/tests/test_collections.py

-from pprint import pprint
-import os
-
-from ..models import *
-from .common import ModelTestCase
-from .models import *
-
-# TODO: check if the SQL queries are efficient (all data should be fetched in one query)
-
-class BookCollection(DocumentCollection):
-    model = Book
-    name = "books"
-    select_related = ['publisher', 'extra_info']
-    prefetch_related = ['chapter_set', 'authors', 'tags', 'publisher__links',
-        'category_set', 'publisher__tags']
-    exclude = ['authors__email', 'summary']
-    add_model_name = True
-
-    def dump_obj(self, model, obj, path):
-        # Customization: for tags, use the name string as the representation
-        if model.name == 'Tag':
-            return obj.name
-        # Here only the url
-        if model.name == 'PublisherLink':
-            return obj.url
-        return super(BookCollection, self).dump_obj(model, obj, path)
-
-
-class CollectionTest(ModelTestCase):
-
-    def setUp(self):
-        create_test_books()
-        #book = Book.objects.get(title=u"Cooking for Geeks")
-        #m = book._meta
-        #import code; _d = dict(); _d.update(globals()); _d.update(locals()); code.interact(local=_d)
-
-    def test_dump(self):
-        bookcol = BookCollection()
-        book = Book.objects.get(title=u"Cooking for Geeks")
-        doc = bookcol.dump(book)
-
-        # Basic fields
-        self.assertTrue('id' in doc)
-        self.assertTrue('title' in doc)
-        self.assertFalse('summary' in doc, "The summary was not excluded")
-        self.assertTrue('year' in doc)
-        self.assertTrue('created' in doc)
-        self.assertEqual(doc['id'], book.id)
-
-        # Relational fields
-        self.assertTrue('publisher' in doc, doc)
-        self.assertEqual(doc['publisher']['name'], u"O'Reilly")
-        self.assertTrue('authors' in doc, doc)
-        self.assertEqual(len(doc['authors']), 1)
-        self.assertTrue('chapter_set' in doc, doc)
-        self.assertEqual(len(doc['chapter_set']), 7)
-
-        # Other expectations
-
-        # - Tags are pure strings
-        self.assertEqual(doc['tags'], [u'cooking', u'geeks', u'technology'])
-
-        # - ForeignKeys not explicitly followed are included as a pure id
-        chapter = doc['chapter_set'][0]
-        # TODO: we should probably omit it if it's the relation we just followed
-        self.assertTrue('book_id' in chapter,
-            "If a FK is not followed, its id will be included")
-
-        # - Author emails are excluded
-        author = doc['authors'][0]
-        self.assertFalse('email' in author, "The author email was not excluded")
-
-
-    def test_dump_collection(self):
-        bookcol = BookCollection()
-        docs = list(bookcol.dump_collection())
-        self.assertEqual(len(docs), 2)
-        if 'pprint' in os.environ:
-            pprint(docs)
-
-    def test_get_related_models(self):
-        bookcol = BookCollection()
-        deps = bookcol.get_related_models()
-        for rel in bookcol.select_related + bookcol.prefetch_related:
-            # Difference between the filter name and the path name
-            if rel == 'chapter_set':
-                rel = 'chapter'
-            elif rel == 'category_set':
-                rel = 'category'
-            self.assertTrue(rel in deps, "Relation {0} not found!".format(rel))
-        pprint(deps)
-        self.assertIs(deps['publisher']['model']._model, Publisher)
-        self.assertIs(deps['extra_info']['model']._model, ExtraBookInfo)
-        self.assertIs(deps['chapter']['model']._model, Chapter)
-        self.assertIs(deps['authors']['model']._model, Author)
-        self.assertIs(deps['tags']['model']._model, Tag)
-        self.assertIs(deps['publisher__links']['model']._model, PublisherLink)
-
-
-
-

denormalize/tests/test_django.py

-from pprint import pprint
-import os
-
-from ..models import *
-from .common import ModelTestCase
-from .models import *
-
-
-
-class DjangoORMTest(ModelTestCase):
-
-    def test_reverse_field_registration(self):
-        self.assertTrue(hasattr(A, 'b_set'))
-        self.assertTrue(A._meta.get_field_by_name('b'))
-
-        self.assertTrue(hasattr(A, 'c_set'))
-        self.assertTrue(A._meta.get_field_by_name('c'))
-
-

denormalize/tests/test_django/__init__.py

Empty file added.

denormalize/tests/test_django/common.py

+from django import test
+from . import models
+from .. import common
+
+import logging
+log = logging.getLogger(__name__)
+
+class BookCollection(common.BookCollection):
+    model = models.Book
+
+class ModelTestCase(test.TestCase):
+    # Source:
+    # http://stackoverflow.com/questions/502916/django-how-to-create-a-model
+    test_models_app = 'denormalize.tests.test_django'
+    _test_models_initiated = False
+    models = models
+    collection = BookCollection
+
+    @classmethod
+    def setUpClass(cls, *args, **kwargs):
+        # NOTE: this is not thread-safe
+        cls.collection.model = cls.models.Book
+
+        if not cls._test_models_initiated:
+            cls.create_models_from_app(cls.test_models_app)
+            cls._test_models_initiated = True
+        super(ModelTestCase, cls).setUpClass(*args, **kwargs)
+
+    @classmethod
+    def create_models_from_app(cls, app_name):
+        """
+        Manually create Models (used only for testing) from the specified string app name.
+        Models are loaded from the module "<app_name>.models"
+        """
+        from django.db import connection, DatabaseError
+        from django.db.models.loading import load_app
+
+        app = load_app(app_name)
+        from django.core.management import sql
+        from django.core.management.color import no_style
+        sql = sql.sql_create(app, no_style(), connection)
+        cursor = connection.cursor()
+        for statement in sql:
+            try:
+                cursor.execute(statement)
+            except DatabaseError, excn:
+                log.debug("DatabaseError in statement: %s",
+                    statement, exc_info=True)

denormalize/tests/test_django/models.py

+"""
+Very simple book model, for test purposes.
+
+A book has:
+
+ * One or more authors, that can also be authors of other books (many-to-many)
+ * One publisher (foreign key)
+ * One or more chapters, that are always book specific (reverse foreign key)
+
+"""
+
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+class Tag(models.Model):
+    name = models.CharField(_("name"), max_length=40)
+
+    def __unicode__(self):
+        return self.name
+
+    class Meta:
+        verbose_name = 'tag'
+        verbose_name_plural = 'tags'
+
+
+class Author(models.Model):
+    name = models.CharField(_("name"), max_length=80)
+    email = models.EmailField(_("email"), blank=True)
+
+    def __unicode__(self):
+        return self.name
+
+    class Meta:
+        verbose_name = 'author'
+        verbose_name_plural = 'authors'
+
+
+class Publisher(models.Model):
+    name = models.CharField(_("name"), max_length=80)
+    email = models.EmailField(_("email"), blank=True)
+    tags = models.ManyToManyField(Tag)
+
+    def __unicode__(self):
+        return self.name
+
+    class Meta:
+        verbose_name = 'publisher'
+        verbose_name_plural = 'publishers'
+
+
+class PublisherLink(models.Model):
+    publisher = models.ForeignKey(Publisher, related_name='links')
+    url = models.URLField(_("url"))
+
+    def __unicode__(self):
+        return self.url
+
+    class Meta:
+        verbose_name = 'publisher link'
+        verbose_name_plural = 'publisher links'
+
+
+class Book(models.Model):
+    title = models.CharField(_("title"), max_length=80)
+    year = models.PositiveIntegerField(_("year"), null=True)
+    summary = models.TextField(_("summary"), blank=True)
+    authors = models.ManyToManyField(Author)
+    publisher = models.ForeignKey(Publisher)
+    tags = models.ManyToManyField(Tag)
+    created = models.DateTimeField(_('created on'), auto_now_add=True)
+
+    def __unicode__(self):
+        return 'Book %s' % (self.id,)
+
+    class Meta:
+        verbose_name = 'book'
+        verbose_name_plural = 'books'
+
+
+class Chapter(models.Model):
+    # FIXME: if related_name is not set, the right filter query is 'chapter'...
+    book = models.ForeignKey(Book)#, related_name='chapter_set')
+    title = models.CharField(_("title"), max_length=80)
+
+    def __unicode__(self):
+        return 'Chapter %s' % (self.id,)
+
+    class Meta:
+        verbose_name = 'chapter'
+        verbose_name_plural = 'chapters'
+
+
+class ExtraBookInfo(models.Model):
+    # This is for testing OneToOneField
+    book = models.OneToOneField(Book, related_name='extra_info')
+    isbn = models.CharField(_("isbn"), max_length=80, blank=True)
+
+    def __unicode__(self):
+        return 'ExtraBookInfo %s' % (self.id,)
+
+    class Meta:
+        verbose_name = 'extra book info'
+        verbose_name_plural = 'extra book info'
+
+
+class Category(models.Model):
+    # To create a reverse m2m relationship for testing
+    name = models.CharField(_("name"), max_length=80)
+    books = models.ManyToManyField(Book)
+
+    def __unicode__(self):
+        return self.name
+
+    class Meta:
+        verbose_name = 'category'
+        verbose_name_plural = 'categories'
+
+
+
+def create_test_data():
+    jeff = Author.objects.create(name=u"Jeff Potter")
+    oreilly = Publisher.objects.create(
+        name=u"O'Reilly",
+        email='orders@oreilly.com',
+    )
+    PublisherLink.objects.create(publisher=oreilly, url='http://oreilly.com/')
+    cooking_for_geeks = Book.objects.create(
+        title=u"Cooking for Geeks",
+        publisher=oreilly,
+        year=2010
+    )
+    cooking_for_geeks.authors = [jeff]
+    cooking_for_geeks.tags = [
+        Tag.objects.get_or_create(name="cooking")[0],
+        Tag.objects.get_or_create(name="geeks")[0],
+        Tag.objects.get_or_create(name="technology")[0],
+    ]
+    ExtraBookInfo.objects.create(book=cooking_for_geeks, isbn='978-0-596-80588-3')
+    chapters = [
+        u"Hello, Kitchen!",
+        u"Initializing the Kitchen",
+        u"Choosing Your Inputs: Flavors and Ingredients",
+        u"Time and Temperature: Cooking's Primary Variables",
+        u"Air: Baking's Key Variable",
+        u"Playing with Chemicals",
+        u"Fun with Hardware"
+    ]
+    for title in chapters:
+        Chapter.objects.create(book=cooking_for_geeks, title=title)
+    oreilly.tags = [Tag.objects.get_or_create(name="technology")[0]]
+
+    kristina = Author.objects.create(name=u"Kristina Chodorow")
+    michael = Author.objects.create(name=u"Michael Dirolf")
+    mongodb = Book.objects.create(
+        title=u"MongoDB: The Definitive Guide",
+        publisher=oreilly,
+        year=2010
+    )
+    mongodb.authors = [kristina, michael]
+    mongodb.tags = [
+        Tag.objects.get_or_create(name="mongodb")[0],
+        Tag.objects.get_or_create(name="databases")[0],
+        Tag.objects.get_or_create(name="technology")[0],
+    ]
+    ExtraBookInfo.objects.create(book=mongodb, isbn='978-1-4493-4468-9')
+    chapters = [u"Introduction", u"Getting Started", u"Querying"]
+    for title in chapters:
+        Chapter.objects.create(book=mongodb, title=title)
+
+    tech = Category.objects.create(name=u"Technology")
+    tech.books.add(cooking_for_geeks)
+    tech.books.add(mongodb)
+
+    return {'book': Book.objects.get(title=u"Cooking for Geeks")}
+
+################################################################
+# To test some django reverse behavior
+
+class A(models.Model):
+    x = models.CharField(max_length=80)
+
+class B(models.Model):
+    a = models.ForeignKey(A)
+    x = models.CharField(max_length=80)
+
+class CBase(models.Model):
+    a = models.ForeignKey(A)
+    class Meta:
+        abstract = True
+
+class C(CBase):
+    x = models.CharField(max_length=80)

denormalize/tests/test_django/test_collections.py

+from ..common import CollectionTestMixin
+from .common import ModelTestCase
+from . import models
+
+# CollectionTestMixin must come first so that its setUp gets called
+class DjangoCollectionTest(CollectionTestMixin, ModelTestCase):
+    pass
+
+class DjangoORMTest(ModelTestCase):
+
+    def test_reverse_field_registration(self):
+        self.assertTrue(hasattr(models.A, 'b_set'))
+        self.assertTrue(models.A._meta.get_field_by_name('b'))
+
+        self.assertTrue(hasattr(models.A, 'c_set'))
+        self.assertTrue(models.A._meta.get_field_by_name('c'))

denormalize/tests/test_sqlalchemy/__init__.py

Empty file added.

denormalize/tests/test_sqlalchemy/common.py

+from .. import common
+from . import models
+
+import unittest2
+import logging
+log = logging.getLogger(__name__)
+
+class BookCollection(common.BookCollection):
+    model = models.Book
+
+class ModelTestCase(unittest2.TestCase):
+    _test_models_initiated = False
+    models = models
+    collection = BookCollection
+
+    @classmethod
+    def setUpClass(cls, *args, **kwargs):
+        # NOTE: this is not thread-safe
+        cls.collection.model = cls.models.Book
+
+        if not cls._test_models_initiated:
+            cls.create_models()
+            cls._test_models_initiated = True
+        super(ModelTestCase, cls).setUpClass(*args, **kwargs)
+
+    @classmethod
+    def create_models(cls):
+        models.Base.metadata.create_all(models.engine)

denormalize/tests/test_sqlalchemy/models.py

+"""
+Very simple book model, for test purposes.
+
+A book has:
+
+ * One or more authors, that can also be authors of other books (many-to-many)
+ * One publisher (foreign key)
+ * One or more chapters, that are always book specific (reverse foreign key)
+
+"""
+
+import sqlalchemy
+from sqlalchemy import types as sqltypes
+try:
+    from sqlalchemy.ext.declarative import declarative_base # 0.7
+except ImportError:
+    from sqlalchemy.ext.declarative.base import declarative_base # 0.9
+from sqlalchemy import create_engine, Table, Column, Integer, String, \
+    DateTime, Text, ForeignKey
+from sqlalchemy.orm import sessionmaker, relationship, synonym, backref
+from sqlalchemy.ext.hybrid import hybrid_property
+
+import datetime
+
+Base = declarative_base()
+
+engine = create_engine('sqlite://')
+
+Session = sessionmaker(bind=engine)
+
+def associate(table1, table2):
+    return Table(
+        'link_%s_%s' % (table1, table2), Base.metadata,
+        Column(table1 + '_id', Integer, ForeignKey(table1 + '.id')),
+        Column(table2 + '_id', Integer, ForeignKey(table2 + '.id'))
+    )
+
+def get_or_create(session, model, **kwargs):
+    instance = session.query(model).filter_by(**kwargs).first()
+    if instance:
+        return instance
+    else:
+        instance = model(**kwargs)
+        session.add(instance)
+        return instance
+
+class Tag(Base):
+    __tablename__ = 'tag'
+
+    id = Column(Integer, primary_key=True)
+    name = Column(String(40))
+
+    def __unicode__(self):
+        return self.name
+
+    # class Meta:
+    #     verbose_name = 'tag'
+    #     verbose_name_plural = 'tags'
+
+
+class Author(Base):
+    __tablename__ = 'author'
+
+    id = Column(Integer, primary_key=True)
+    name = Column(String(80))
+    email = Column(String)
+
+    def __unicode__(self):
+        return self.name
+
+    # class Meta:
+    #     verbose_name = 'author'
+    #     verbose_name_plural = 'authors'
+
+
+class Publisher(Base):
+    __tablename__ = 'publisher'
+
+    id = Column(Integer, primary_key=True)
+    name = Column(String(80))
+    email = Column(String)
+    tags = relationship(Tag, secondary=associate('publisher', 'tag'),
+                        backref='publishers')
+
+    def __unicode__(self):
+        return self.name
+
+    # class Meta:
+    #     verbose_name = 'publisher'
+    #     verbose_name_plural = 'publishers'
+
+
+class PublisherLink(Base):
+    __tablename__ = 'publisher_link'
+
+    # Note: this uses a foreign key as the primary key
+    id = Column(Integer, ForeignKey('publisher.id'), primary_key=True)
+    # many-to-one:
+    publisher = relationship(Publisher, backref='links')
+
+    url = Column(String)
+
+    def __unicode__(self):
+        return self.url
+
+    # class Meta:
+    #     verbose_name = 'publisher link'
+    #     verbose_name_plural = 'publisher links'
+
+
+class Book(Base):
+    __tablename__ = 'book'
+
+    id = Column(Integer, primary_key=True)
+    title = Column(String(80))
+    # year = PositiveIntegerField(_("year"), nullable=True)
+    year = Column(Integer, nullable=True)
+    summary = Column(Text)
+    authors = relationship(Author, secondary=associate('book', 'author'),
+                           backref='books')
+
+    # many-to-one:
+    publisher_id = Column(Integer, ForeignKey('publisher.id'))
+    publisher = relationship(Publisher, backref='books')
+
+    tags = relationship(Tag, secondary=associate('book', 'tag'),
+                        backref='books')
+    created = Column(DateTime, default=datetime.datetime.utcnow)
+
+    def __unicode__(self):
+        return 'Book %s' % (self.id,)
+
+    # class Meta:
+    #     verbose_name = 'book'
+    #     verbose_name_plural = 'books'
+
+
+class Chapter(Base):
+    __tablename__ = 'chapter'
+
+    id = Column(Integer, primary_key=True)
+
+    # many-to-one:
+    book_id = Column(Integer, ForeignKey('book.id'))
+    book = relationship(Book, backref='chapter_set')
+
+    title = Column(String(80))
+
+    def __unicode__(self):
+        return 'Chapter %s' % (self.id,)
+
+    # class Meta:
+    #     verbose_name = 'chapter'
+    #     verbose_name_plural = 'chapters'
+
+
+class ExtraBookInfo(Base):
+    __tablename__ = 'extra_book_info'
+
+    id = Column(Integer, primary_key=True)
+    # one-to-one:
+    book_id = Column(Integer, ForeignKey('book.id'))
+    book = relationship(Book, backref=backref("extra_info", uselist=False))
+
+    isbn = Column(String(80))
+
+    def __unicode__(self):
+        return 'ExtraBookInfo %s' % (self.id,)
+
+    # class Meta:
+    #     verbose_name = 'extra book info'
+    #     verbose_name_plural = 'extra book info'
+
+
+class Category(Base):
+    __tablename__ = 'category'
+
+    id = Column(Integer, primary_key=True)
+
+    # To create a reverse m2m relationship for testing
+    name = Column(String(80))
+    books = relationship(Book, secondary=associate('category', 'book'),
+                         backref='categories')
+
+    def __unicode__(self):
+        return self.name
+
+    # class Meta:
+    #     verbose_name = 'category'
+    #     verbose_name_plural = 'categories'
+
+
+def create_test_data():
+    print "SQLALC CREATE TEST DATA!!!!!!!!!"
+    session = Session()
+
+    jeff = Author(name=u"Jeff Potter")
+    oreilly = Publisher(
+        name=u"O'Reilly",
+        email='orders@oreilly.com',
+    )
+    oreilly.links = [PublisherLink(url='http://oreilly.com/')]
+
+    cooking_for_geeks = Book(
+        title=u"Cooking for Geeks",
+        publisher=oreilly,
+        year=2010
+    )
+    cooking_for_geeks.authors = [jeff]
+    cooking_for_geeks.tags = [
+        get_or_create(session, Tag, name="cooking"),
+        get_or_create(session, Tag, name="geeks"),
+        get_or_create(session, Tag, name="technology"),
+    ]
+    cooking_for_geeks.extra_info = ExtraBookInfo(isbn='978-0-596-80588-3')
+    chapters = [
+        u"Hello, Kitchen!",
+        u"Initializing the Kitchen",
+        u"Choosing Your Inputs: Flavors and Ingredients",
+        u"Time and Temperature: Cooking's Primary Variables",
+        u"Air: Baking's Key Variable",
+        u"Playing with Chemicals",
+        u"Fun with Hardware"
+    ]
+    for title in chapters:
+        cooking_for_geeks.chapter_set.append(Chapter(title=title))
+    oreilly.tags = [get_or_create(session, Tag, name="technology")]
+
+    kristina = Author(name=u"Kristina Chodorow")
+    michael = Author(name=u"Michael Dirolf")
+    mongodb = Book(
+        title=u"MongoDB: The Definitive Guide",
+        publisher=oreilly,
+        year=2010
+    )
+    mongodb.authors = [kristina, michael]
+    mongodb.tags = [
+        get_or_create(session, Tag, name="mongodb"),
+        get_or_create(session, Tag, name="databases"),
+        get_or_create(session, Tag, name="technology"),
+    ]
+
+    ExtraBookInfo(book=mongodb, isbn='978-1-4493-4468-9')
+    chapters = [u"Introduction", u"Getting Started", u"Querying"]
+    for title in chapters:
+        Chapter(book=mongodb, title=title)
+
+    tech = Category(name=u"Technology")
+    tech.books.append(cooking_for_geeks)
+    tech.books.append(mongodb)
+
+    session.add(cooking_for_geeks)
+    session.add(mongodb)
+    session.commit()
+    fresh_book = session.query(Book).filter_by(title=u"Cooking for Geeks").one()
+    return {'book': fresh_book}
+
+
+################################################################
+# To test some django reverse behavior
+
+# class A(Base):
+#     x = Column(String(80))
+
+# class B(Base):
+#     a = ForeignKey(A)
+#     x = Column(String(80))
+
+# class CBase(Base):
+#     a = ForeignKey(A)
+#     class Meta:
+#         abstract = True
+
+# class C(CBase):
+#     x = Column(String(80))

denormalize/tests/test_sqlalchemy/test_collections.py

+from ..common import CollectionTestMixin
+from .common import ModelTestCase
+from . import models
+from pprint import pprint
+import os
+
+# CollectionTestMixin must come first so that its setUp gets called
+class SqlAlchemyCollectionTest(CollectionTestMixin, ModelTestCase):
+    models = models
+
+    def test_dump_collection(self):
+        # override this for now until we sort out querysets
+        bookcol = self.collection()
+        session = models.Session()
+
+        q = session.query(models.Book)
+        docs = [bookcol.dump(obj) for obj in q.all()]
+        self.assertEqual(len(docs), 2)
+        if 'pprint' in os.environ:
+            pprint(docs)
+
+    @classmethod
+    def tearDownClass(cls):
+        print '-'*30
+        print "TEARING DOWN"
+        print '-'*30
+        meta = models.Base.metadata
+        for table in reversed(meta.sorted_tables):
+            models.engine.execute(table.delete())
+        super(SqlAlchemyCollectionTest, cls).tearDownClass()
+
+# class DjangoORMTest(ModelTestCase):
+#     def test_reverse_field_registration(self):
+#         self.assertTrue(hasattr(models.A, 'b_set'))
+#         self.assertTrue(models.A._meta.get_field_by_name('b'))
+
+#         self.assertTrue(hasattr(models.A, 'c_set'))
+#         self.assertTrue(models.A._meta.get_field_by_name('c'))

test_project/test_project/testapp/management/commands/testdata.py

 from django.core.management.base import BaseCommand
 
-from ...test_models import create_test_books
+from ...test_models import create_test_data
 
 class Command(BaseCommand):
     help = 'Load test data'
 
     def handle(self, *args, **options):
-        create_test_books()
+        create_test_data()
         print 'Done'

test_project/test_project/testapp/models.py

 import inspect
-from denormalize.tests import test_collections
+from denormalize.tests import common
 from denormalize.backend.locmem import LocMemBackend
 
 from test_models import *
 
-class BookCollection(test_collections.BookCollection):
+class BookCollection(common.BookCollection):
     model = Book
 
 col = BookCollection()
 backend = LocMemBackend(name='locmem')
-backend.register(col)
+backend.register(col)

test_project/test_project/testapp/test_models.py

-../../../denormalize/tests/models.py
+../../../denormalize/tests/test_django/models.py