diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0b27f55..ec7304d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog ========= +Version 1.9.0 (Dec 21, 2015) +------------- + +* Added support for Django 1.9 in the db package + Version 1.8.0 (Jul 12, 2015) ------------- diff --git a/djangotoolbox/db/base.py b/djangotoolbox/db/base.py index ab7b9ab..4cb8174 100644 --- a/djangotoolbox/db/base.py +++ b/djangotoolbox/db/base.py @@ -1,9 +1,16 @@ -from django.utils.six.moves import cPickle as pickle import datetime +import django from django.conf import settings +from django.db.utils import DatabaseError +from django.utils import six +from django.utils import timezone +from django.utils.functional import Promise + +from django.utils.six.moves import cPickle as pickle + +from .creation import NonrelDatabaseCreation -import django if django.VERSION < (1, 8): from django.db.backends import ( @@ -21,9 +28,6 @@ from django.db.backends.base.introspection import BaseDatabaseIntrospection from django.db.backends.base.operations import BaseDatabaseOperations -from django.db.utils import DatabaseError -from django.utils import timezone -from django.utils.functional import Promise if django.VERSION < (1, 5): from django.utils.encoding import (smart_unicode as smart_text, @@ -39,8 +43,6 @@ else: from django.utils.safestring import SafeBytes, SafeText, EscapeBytes, EscapeText -from .creation import NonrelDatabaseCreation - class NonrelDatabaseFeatures(BaseDatabaseFeatures): # Most NoSQL databases don't have true transaction support. @@ -459,7 +461,7 @@ def _value_for_db_collection(self, value, field, field_kind, db_type, value = ( (key, self._value_for_db(subvalue, subfield, subkind, db_subtype, lookup)) - for key, subvalue in value.iteritems()) + for key, subvalue in six.iteritems(value)) # Return just a dict, a once-flattened list; if db_type == 'dict': @@ -514,9 +516,9 @@ def _value_from_db_collection(self, value, field, field_kind, db_type): # Generator yielding pairs with deconverted values, the # "list" db_type stores keys and values interleaved. if db_type == 'list': - value = zip(value[::2], value[1::2]) + value = list(zip(value[::2], value[1::2])) else: - value = value.iteritems() + value = iter(six.iteritems(value)) # DictField needs to hold a dict. return dict( @@ -575,7 +577,7 @@ def _value_for_db_model(self, value, field, field_kind, db_type, lookup): value = ( (subfield.column, self._value_for_db( subvalue, lookup=lookup, *self._convert_as(subfield, lookup))) - for subfield, subvalue in value.iteritems()) + for subfield, subvalue in six.iteritems(value)) # Cast to a dict, interleave columns with values on a list, # serialize, or return a generator. @@ -603,7 +605,7 @@ def _value_from_db_model(self, value, field, field_kind, db_type): # Separate keys from values and create a dict or unpickle one. if db_type == 'list': - value = dict(zip(value[::2], value[1::2])) + value = dict(list(zip(value[::2], value[1::2]))) elif db_type == 'bytes' or db_type == 'string': value = pickle.loads(value) diff --git a/djangotoolbox/db/basecompiler.py b/djangotoolbox/db/basecompiler.py index d49a4f9..e8fdec4 100644 --- a/djangotoolbox/db/basecompiler.py +++ b/djangotoolbox/db/basecompiler.py @@ -2,6 +2,7 @@ import django from django.conf import settings +from django.db import connections from django.db.models.fields import NOT_PROVIDED from django.db.models.query import QuerySet from django.db.models.sql.compiler import SQLCompiler @@ -9,7 +10,7 @@ from django.db.models.sql.where import AND, OR from django.db.utils import DatabaseError, IntegrityError from django.utils.tree import Node -from django.db import connections + try: from django.db.models.sql.where import SubqueryConstraint @@ -177,7 +178,7 @@ def add_filters(self, filters): if filters.negated: self._negated = not self._negated - # ---------------------------------------------- + # ----------------------------------------------s # Internal API for reuse by subclasses # ---------------------------------------------- @@ -233,7 +234,7 @@ def _decode_child(self, child): raise DatabaseError("This database doesn't support filtering " "on non-primary key ForeignKey fields.") - field = (f for f in opts.fields if f.column == column).next() + field = next(f for f in opts.fields if f.column == column) assert field.rel is not None value = self._normalize_lookup_value( @@ -421,7 +422,10 @@ def execute_sql(self, result_type=MULTI): """ self.pre_sql_setup() - aggregates = self.query.aggregate_select.values() + if django.VERSION < (1, 8): + aggregates = list(self.query.aggregate_select.values()) + else: + aggregates = list(self.query.annotation_select.values()) # Simulate a count(). if aggregates: @@ -441,8 +445,18 @@ def execute_sql(self, result_type=MULTI): raise DatabaseError("This database backend only supports " "count() queries on the primary key.") else: + try: + from django.db.models.expressions import Star + + try: + is_star = isinstance(aggregate.input_field, Star) # Django 1.8.5+ + except AttributeError: + is_star = isinstance(aggregate.get_source_expressions()[0], Star) # Django 1.9 + except ImportError: + is_star = aggregate.input_field.value == '*' + # Fair warning: the latter part of this or statement hasn't been tested - if aggregate.input_field.value != '*' and aggregate.input_field != (opts.db_table, opts.pk.column): + if not is_star and aggregate.input_field != (opts.db_table, opts.pk.column): raise DatabaseError("This database backend only supports " "count() queries on the primary key.") @@ -474,6 +488,9 @@ def _make_result(self, entity, fields): # This is the default behavior of ``query.convert_values`` # until django 1.8, where multiple converters are a thing. value = self.connection.ops.convert_values(value, field) + + if hasattr(field, "from_db_value"): + value = field.from_db_value(value, None, self.connection, None) if value is None and not field.null: raise IntegrityError("Non-nullable field %s can't be None!" % field.name) @@ -492,8 +509,10 @@ def check_query(self): """ if hasattr(self.query, 'is_empty') and self.query.is_empty(): raise EmptyResultSet() - if (len([a for a in self.query.alias_map if self.query.alias_refcount[a]]) > 1 - or self.query.distinct or self.query.extra or self.query.having): + if (len([a for a in self.query.alias_map if self.query.alias_refcount[a]]) > 1 or + self.query.distinct or self.query.extra): # or self.having -- Not quite working. + # having is no longer part of the query as of 1.9; It moved to the compiler + # https://github.com/django/django/commit/afe0bb7b13bb8dc4370f32225238012c873b0ee3 raise DatabaseError("This query is not supported by the database.") def get_count(self, check_exists=False): @@ -547,9 +566,9 @@ def get_fields(self): only_load = self.deferred_to_columns() if only_load: db_table = self.query.model._meta.db_table - only_load = dict((k, v) for k, v in only_load.items() + only_load = dict((k, v) for k, v in list(only_load.items()) if v or k == db_table) - if len(only_load.keys()) > 1: + if len(list(only_load.keys())) > 1: raise DatabaseError("Multi-table inheritance is not " "supported by non-relational DBs %s." % repr(only_load)) diff --git a/djangotoolbox/fields.py b/djangotoolbox/fields.py index c4e877b..f63b400 100644 --- a/djangotoolbox/fields.py +++ b/djangotoolbox/fields.py @@ -1,11 +1,12 @@ # All fields except for BlobField written by Jonas Haag from django.core.exceptions import ValidationError -from django.utils.importlib import import_module from django.db import models +from django.db.models.fields.related import add_lazy_relation from django.db.models.fields.subclassing import Creator from django.db.utils import IntegrityError -from django.db.models.fields.related import add_lazy_relation +from django.utils import six +from django.utils.importlib import import_module __all__ = ('RawField', 'ListField', 'SetField', 'DictField', @@ -85,7 +86,7 @@ def contribute_to_class(self, cls, name): if item_metaclass and issubclass(item_metaclass, models.SubfieldBase): setattr(cls, self.name, Creator(self)) - if isinstance(self.item_field, models.ForeignKey) and isinstance(self.item_field.rel.to, basestring): + if isinstance(self.item_field, models.ForeignKey) and isinstance(self.item_field.rel.to, six.string_types): """ If rel.to is a string because the actual class is not yet defined, look up the actual class later. Refer to django.models.fields.related.RelatedField.contribute_to_class. @@ -225,7 +226,7 @@ def get_internal_type(self): def _map(self, function, iterable, *args, **kwargs): return self._type((key, function(value, *args, **kwargs)) - for key, value in iterable.iteritems()) + for key, value in six.iteritems(iterable)) def validate(self, values, model_instance): if not isinstance(values, dict): @@ -233,6 +234,7 @@ def validate(self, values, model_instance): type(values)) +@six.add_metaclass(models.SubfieldBase) class EmbeddedModelField(models.Field): """ Field that allows you to embed a model instance. @@ -245,7 +247,6 @@ class EmbeddedModelField(models.Field): the embedded instance (not just pre_save, get_db_prep_* and to_python). """ - __metaclass__ = models.SubfieldBase def __init__(self, embedded_model=None, *args, **kwargs): self.embedded_model = embedded_model @@ -255,7 +256,6 @@ def __init__(self, embedded_model=None, *args, **kwargs): def get_internal_type(self): return 'EmbeddedModelField' - def _set_model(self, model): """ Resolves embedded model class once the field knows the model it @@ -271,7 +271,7 @@ def _set_model(self, model): our "model" attribute in its contribute_to_class method). """ self._model = model - if model is not None and isinstance(self.embedded_model, basestring): + if model is not None and isinstance(self.embedded_model, six.string_types): def _resolve_lookup(self_, resolved_model, model): self.embedded_model = resolved_model @@ -280,7 +280,6 @@ def _resolve_lookup(self_, resolved_model, model): model = property(lambda self: self._model, _set_model) - def stored_model(self, column_values): """ Returns the fixed embedded_model this field was initialized diff --git a/djangotoolbox/http.py b/djangotoolbox/http.py index ac5f896..e86fcff 100644 --- a/djangotoolbox/http.py +++ b/djangotoolbox/http.py @@ -1,7 +1,8 @@ +import json + from django.conf import settings from django.core.serializers.json import DjangoJSONEncoder from django.http import HttpResponse -from django.utils import simplejson from django.utils.encoding import force_unicode from django.utils.functional import Promise @@ -18,9 +19,8 @@ class JSONResponse(HttpResponse): def __init__(self, pyobj, **kwargs): super(JSONResponse, self).__init__( - simplejson.dumps(pyobj, cls=LazyEncoder), - content_type='application/json; charset=%s' % - settings.DEFAULT_CHARSET, + json.dumps(pyobj, cls=LazyEncoder), + content_type='application/json; charset=%s' % settings.DEFAULT_CHARSET, **kwargs) diff --git a/djangotoolbox/test.py b/djangotoolbox/test.py index 8228acb..52a0234 100644 --- a/djangotoolbox/test.py +++ b/djangotoolbox/test.py @@ -1,12 +1,13 @@ +from unittest import TextTestResult, TextTestRunner + from django.test import TestCase -from django.utils.unittest import TextTestResult, TextTestRunner try: from django.test.runner import DiscoverRunner as TestRunner except ImportError: from django.test.simple import DjangoTestSuiteRunner as TestRunner -from .utils import object_list_to_table +from .utils import object_list_to_table, equal_lists import re diff --git a/djangotoolbox/tests.py b/djangotoolbox/tests.py index c3bd8a8..f834b2a 100644 --- a/djangotoolbox/tests.py +++ b/djangotoolbox/tests.py @@ -1,6 +1,6 @@ -from __future__ import with_statement from decimal import Decimal, InvalidOperation import time +from unittest import expectedFailure, skip from django.core import serializers from django.db import models @@ -9,7 +9,7 @@ from django.db.utils import DatabaseError from django.dispatch.dispatcher import receiver from django.test import TestCase -from django.utils.unittest import expectedFailure, skip +from django.utils import six from .fields import ListField, SetField, DictField, EmbeddedModelField @@ -99,12 +99,12 @@ class IterableFieldsTest(TestCase): unordered_ints = [4, 2, 6, 1] def setUp(self): - for i, float in zip(range(1, 5), IterableFieldsTest.floats): + for i, float in zip(list(range(1, 5)), IterableFieldsTest.floats): ListModel(integer=i, floating_point=float, names=IterableFieldsTest.names[:i]).save() def test_startswith(self): - self.assertEquals( + self.assertEqual( dict([(entity.pk, entity.names) for entity in ListModel.objects.filter(names__startswith='Sa')]), dict([(3, ['Kakashi', 'Naruto', 'Sasuke']), @@ -142,7 +142,7 @@ def test_ordering(self): self.assertLessEqual(f.ordering.calls, len(self.unordered_ints)) def test_gt(self): - self.assertEquals( + self.assertEqual( dict([(entity.pk, entity.names) for entity in ListModel.objects.filter(names__gt='Kakashi')]), dict([(2, [u'Kakashi', u'Naruto']), @@ -150,7 +150,7 @@ def test_gt(self): (4, [u'Kakashi', u'Naruto', u'Sasuke', u'Sakura']), ])) def test_lt(self): - self.assertEquals( + self.assertEqual( dict([(entity.pk, entity.names) for entity in ListModel.objects.filter(names__lt='Naruto')]), dict([(1, [u'Kakashi']), @@ -159,14 +159,14 @@ def test_lt(self): (4, [u'Kakashi', u'Naruto', u'Sasuke', u'Sakura']), ])) def test_gte(self): - self.assertEquals( + self.assertEqual( dict([(entity.pk, entity.names) for entity in ListModel.objects.filter(names__gte='Sakura')]), dict([(3, [u'Kakashi', u'Naruto', u'Sasuke']), (4, [u'Kakashi', u'Naruto', u'Sasuke', u'Sakura']), ])) def test_lte(self): - self.assertEquals( + self.assertEqual( dict([(entity.pk, entity.names) for entity in ListModel.objects.filter(names__lte='Kakashi')]), dict([(1, [u'Kakashi']), @@ -175,41 +175,41 @@ def test_lte(self): (4, [u'Kakashi', u'Naruto', u'Sasuke', u'Sakura']), ])) def test_equals(self): - self.assertEquals([entity.names for entity in - ListModel.objects.filter(names='Sakura')], - [[u'Kakashi', u'Naruto', u'Sasuke', u'Sakura']]) + self.assertEqual([entity.names for entity in + ListModel.objects.filter(names='Sakura')], + [[u'Kakashi', u'Naruto', u'Sasuke', u'Sakura']]) # Test with additonal pk filter (for DBs that have special pk # queries). query = ListModel.objects.filter(names='Sakura') - self.assertEquals(query.get(pk=query[0].pk).names, - [u'Kakashi', u'Naruto', u'Sasuke', u'Sakura']) + self.assertEqual(query.get(pk=query[0].pk).names, + [u'Kakashi', u'Naruto', u'Sasuke', u'Sakura']) def test_is_null(self): - self.assertEquals(ListModel.objects.filter( + self.assertEqual(ListModel.objects.filter( names__isnull=True).count(), 0) def test_exclude(self): - self.assertEquals( + self.assertEqual( dict([(entity.pk, entity.names) for entity in ListModel.objects.all().exclude(names__lt='Sakura')]), dict([(3, [u'Kakashi', u'Naruto', u'Sasuke']), (4, [u'Kakashi', u'Naruto', u'Sasuke', u'Sakura']), ])) def test_chained_filter(self): - self.assertEquals( + self.assertEqual( [entity.names for entity in ListModel.objects .filter(names='Sasuke').filter(names='Sakura')], [['Kakashi', 'Naruto', 'Sasuke', 'Sakura'], ]) - self.assertEquals( + self.assertEqual( [entity.names for entity in ListModel.objects .filter(names__startswith='Sa').filter(names='Sakura')], [['Kakashi', 'Naruto', 'Sasuke', 'Sakura']]) # Test across multiple columns. On app engine only one filter # is allowed to be an inequality filter. - self.assertEquals( + self.assertEqual( [entity.names for entity in ListModel.objects .filter(floating_point=9.1).filter(names__startswith='Sa')], [['Kakashi', 'Naruto', 'Sasuke'], ]) @@ -217,7 +217,7 @@ def test_chained_filter(self): def test_setfield(self): setdata = [1, 2, 3, 2, 1] # At the same time test value conversion. - SetModel(setfield=map(str, setdata)).save() + SetModel(setfield=list(map(str, setdata))).save() item = SetModel.objects.filter(setfield=3)[0] self.assertEqual(item.setfield, set(setdata)) # This shouldn't raise an error because the default value is @@ -233,7 +233,7 @@ def test_dictfield(self): dt = item.auto_now['a'] self.assertNotEqual(dt, None) item.save() - time.sleep(0.5) # Sleep to avoid false positive failure on the assertion below + time.sleep(0.5) # Sleep to avoid false positive failure on the assertion below self.assertGreater(DictModel.objects.get().auto_now['a'], dt) item.delete() @@ -245,7 +245,7 @@ def test_dictfield(self): @skip("GAE specific?") def test_Q_objects(self): - self.assertEquals( + self.assertEqual( [entity.names for entity in ListModel.objects .exclude(Q(names__lt='Sakura') | Q(names__gte='Sasuke'))], [['Kakashi', 'Naruto', 'Sasuke', 'Sakura']]) @@ -253,7 +253,7 @@ def test_Q_objects(self): def test_list_with_foreignkeys(self): class ReferenceList(models.Model): - keys = ListField(models.ForeignKey('Model')) + keys = ListField(models.ForeignKey('Model')) class Model(models.Model): pass @@ -333,7 +333,7 @@ def _test_pre_save(self, instance, get_field): self.assertNotEqual(auto_now, None) self.assertNotEqual(auto_now_add, None) - sleep(1) # FIXME + sleep(1) # FIXME instance.save() self.assertNotEqualDatetime(get_field(instance).auto_now, get_field(instance).auto_now_add) @@ -386,24 +386,24 @@ def test_error_messages(self): ({'simple': 42}, EmbeddedModel), ({'simple_untyped': 42}, models.Model), ({'typed_list': [EmbeddedModel()]}, SetModel)): - self.assertRaisesRegexp( + self.assertRaisesRegex( TypeError, "Expected instance of type %r." % expected, EmbeddedModelFieldModel(**kwargs).save) def test_typed_listfield(self): EmbeddedModelFieldModel.objects.create( - typed_list=[SetModel(setfield=range(3)), - SetModel(setfield=range(9))], - ordered_list=[Target(index=i) for i in xrange(5, 0, -1)]) + typed_list=[SetModel(setfield=list(range(3)), + SetModel(setfield=list(range(9))], + ordered_list=[Target(index=i) for i in six.moves.range(5, 0, -1)]) obj = EmbeddedModelFieldModel.objects.get() self.assertIn(5, obj.typed_list[1].setfield) self.assertEqual([target.index for target in obj.ordered_list], - range(1, 6)) + list(range(1, 6))) def test_untyped_listfield(self): EmbeddedModelFieldModel.objects.create(untyped_list=[ EmbeddedModel(someint=7), - OrderedListModel(ordered_ints=range(5, 0, -1)), + OrderedListModel(ordered_ints=six.moves.range(5, 0, -1)), SetModel(setfield=[1, 2, 2, 3])]) instances = EmbeddedModelFieldModel.objects.get().untyped_list for instance, cls in zip(instances, @@ -414,7 +414,7 @@ def test_untyped_listfield(self): def test_untyped_dict(self): EmbeddedModelFieldModel.objects.create(untyped_dict={ - 'a': SetModel(setfield=range(3)), + 'a': SetModel(setfield=list(range(3))), 'b': DictModel(dictfield={'a': 1, 'b': 2}), 'c': DictModel(dictfield={}, auto_now={'y': 1})}) data = EmbeddedModelFieldModel.objects.get().untyped_dict @@ -716,15 +716,14 @@ def test_filter(self): d = DecimalModel.objects.get(decimal=Decimal('5.0')) self.assertTrue(isinstance(d.decimal, Decimal)) - self.assertEquals(str(d.decimal), '5.00') + self.assertEqual(str(d.decimal), '5.00') d = DecimalModel.objects.get(decimal=Decimal('45.60')) - self.assertEquals(str(d.decimal), '45.60') + self.assertEqual(str(d.decimal), '45.60') - # Filter argument should be converted to Decimal with 2 decimal - #_places. + # Filter argument should be converted to Decimal with 2 decimal places. d = DecimalModel.objects.get(decimal='0000345.67333333333333333') - self.assertEquals(str(d.decimal), '345.67') + self.assertEqual(str(d.decimal), '345.67') def test_order(self): """ @@ -733,7 +732,7 @@ def test_order(self): """ rows = DecimalModel.objects.all().order_by('decimal') values = list(d.decimal for d in rows) - self.assertEquals(values, sorted(values)) + self.assertEqual(values, sorted(values)) def test_sign_extend(self): DecimalModel(decimal=Decimal('-0.0')).save() @@ -750,6 +749,7 @@ class DeleteModel(models.Model): key = models.IntegerField(primary_key=True) deletable = models.BooleanField() + class BasicDeleteTest(TestCase): def setUp(self): @@ -766,22 +766,24 @@ def test_model_delete(self): def test_delete_all(self): DeleteModel.objects.all().delete() - self.assertEquals(0, DeleteModel.objects.all().count()) + self.assertEqual(0, DeleteModel.objects.all().count()) def test_delete_filtered(self): DeleteModel.objects.filter(deletable=True).delete() - self.assertEquals(5, DeleteModel.objects.all().count()) + self.assertEqual(5, DeleteModel.objects.all().count()) class M2MDeleteChildModel(models.Model): key = models.IntegerField(primary_key=True) + class M2MDeleteModel(models.Model): key = models.IntegerField(primary_key=True) deletable = models.BooleanField() children = models.ManyToManyField(M2MDeleteChildModel, blank=True) + class ManyToManyDeleteTest(TestCase): """ Django-nonrel doesn't support many-to-many, but there may be @@ -804,18 +806,19 @@ def test_model_delete(self): def test_delete_all(self): M2MDeleteModel.objects.all().delete() - self.assertEquals(0, M2MDeleteModel.objects.all().count()) + self.assertEqual(0, M2MDeleteModel.objects.all().count()) @expectedFailure def test_delete_filtered(self): M2MDeleteModel.objects.filter(deletable=True).delete() - self.assertEquals(5, M2MDeleteModel.objects.all().count()) + self.assertEqual(5, M2MDeleteModel.objects.all().count()) class QuerysetModel(models.Model): key = models.IntegerField(primary_key=True) + class QuerysetTest(TestCase): """ Django 1.6 changes how diff --git a/setup.py b/setup.py index ad22586..27d1a46 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ pass setup(name='djangotoolbox', - version='1.8.0', + version='1.9.0', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Waldemar Kornewald',