diff --git a/.travis.yml b/.travis.yml index 0c00347..2634fcd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,13 +5,10 @@ python: - "2.7" env: - - DJANGO=Django==1.2.7 SOUTH=1 - - DJANGO=Django==1.3.7 SOUTH=1 - DJANGO=Django==1.4.5 SOUTH=1 - - DJANGO=Django==1.4.5 SOUTH=1 - - DJANGO=Django==1.5 SOUTH=1 + - DJANGO=Django==1.5.1 SOUTH=1 - DJANGO=https://github.com/django/django/tarball/master SOUTH=1 - - DJANGO=Django==1.4.5 SOUTH=0 + - DJANGO=Django==1.5.1 SOUTH=0 install: - pip install $DJANGO --use-mirrors @@ -20,4 +17,15 @@ install: script: coverage run -a --branch --include="model_utils/*" --omit="model_utils/tests/*" setup.py test +matrix: + include: + - python: 3.2 + env: DJANGO=Django==1.5.1 SOUTH=0 + - python: 3.2 + env: DJANGO=https://github.com/django/django/tarball/master SOUTH=0 + - python: 3.3 + env: DJANGO=Django==1.5.1 SOUTH=0 + - python: 3.3 + env: DJANGO=https://github.com/django/django/tarball/master SOUTH=0 + after_success: coveralls diff --git a/CHANGES.rst b/CHANGES.rst index 202e8e6..de41a41 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,8 @@ tip (unreleased) compatibility. - Fix intermittent ``StatusField`` bug. Fixes GH-29. +- Added Python 3 support +- Dropped support for Django 1.2 and 1.3. Django 1.4.2+ required. 1.3.0 (2013.03.27) diff --git a/README.rst b/README.rst index b73ca5d..1ceea79 100644 --- a/README.rst +++ b/README.rst @@ -29,7 +29,8 @@ your ``INSTALLED_APPS`` setting. Dependencies ------------ -``django-model-utils`` is tested with `Django`_ 1.2 and later on Python 2.6 and 2.7. +``django-model-utils`` supports `Django`_ 1.4.2 and later on Python 2.6, 2.7, +3.2, and 3.3. .. _Django: http://www.djangoproject.com/ diff --git a/model_utils/choices.py b/model_utils/choices.py index 65097c7..b5177cd 100644 --- a/model_utils/choices.py +++ b/model_utils/choices.py @@ -1,3 +1,6 @@ +from __future__ import unicode_literals + + class Choices(object): """ A class to encapsulate handy functionality for lists of choices @@ -72,4 +75,4 @@ class Choices(object): def __repr__(self): return '%s(%s)' % (self.__class__.__name__, - ', '.join(("%s" % str(i) for i in self._full))) + ', '.join(("%s" % repr(i) for i in self._full))) diff --git a/model_utils/fields.py b/model_utils/fields.py index 6dd6459..7d46504 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -1,13 +1,9 @@ -from datetime import datetime +from __future__ import unicode_literals from django.db import models from django.conf import settings - - -try: - from django.utils.timezone import now as now -except ImportError: - now = datetime.now +from django.utils.encoding import python_2_unicode_compatible +from django.utils.timezone import now class AutoCreatedField(models.DateTimeField): @@ -128,6 +124,7 @@ def get_excerpt(content): return '\n'.join(default_excerpt) +@python_2_unicode_compatible class SplitText(object): def __init__(self, instance, field_name, excerpt_field_name): # instead of storing actual values store a reference to the instance @@ -153,8 +150,7 @@ class SplitText(object): return self.excerpt.strip() != self.content.strip() has_more = property(_get_has_more) - # allows display via templates without .content necessary - def __unicode__(self): + def __str__(self): return self.content class SplitDescriptor(object): diff --git a/model_utils/managers.py b/model_utils/managers.py index 74cc7bc..7c6c9f9 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals import django from django.db import models from django.db.models.fields.related import OneToOneField @@ -38,7 +39,7 @@ class InheritanceQuerySet(QuerySet): def annotate(self, *args, **kwargs): qset = super(InheritanceQuerySet, self).annotate(*args, **kwargs) - qset._annotated = [a.default_alias for a in args] + kwargs.keys() + qset._annotated = [a.default_alias for a in args] + list(kwargs.keys()) return qset diff --git a/model_utils/models.py b/model_utils/models.py index 40e7aa8..0ad9d32 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -1,20 +1,15 @@ -from datetime import datetime +from __future__ import unicode_literals from django.db import models from django.utils.translation import ugettext_lazy as _ from django.db.models.fields import FieldDoesNotExist from django.core.exceptions import ImproperlyConfigured +from django.utils.timezone import now from model_utils.managers import QueryManager from model_utils.fields import AutoCreatedField, AutoLastModifiedField, \ StatusField, MonitorField -try: - from django.utils.timezone import now as now -except ImportError: - now = datetime.now - - class TimeStampedModel(models.Model): """ diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 1b94abd..d2ce0d1 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -1,4 +1,4 @@ -from __future__ import with_statement +from __future__ import unicode_literals import pickle from datetime import datetime, timedelta @@ -6,6 +6,7 @@ from datetime import datetime, timedelta import django from django.db import models from django.db.models.fields import FieldDoesNotExist +from django.utils.six import text_type from django.core.exceptions import ImproperlyConfigured, FieldError from django.test import TestCase @@ -27,28 +28,28 @@ from model_utils.tests.models import ( class GetExcerptTests(TestCase): def test_split(self): e = get_excerpt("some content\n\n\n\nsome more") - self.assertEquals(e, 'some content\n') + self.assertEqual(e, 'some content\n') def test_auto_split(self): e = get_excerpt("para one\n\npara two\n\npara three") - self.assertEquals(e, 'para one\n\npara two') + self.assertEqual(e, 'para one\n\npara two') def test_middle_of_para(self): e = get_excerpt("some text\n\nmore text") - self.assertEquals(e, 'some text') + self.assertEqual(e, 'some text') def test_middle_of_line(self): e = get_excerpt("some text more text") - self.assertEquals(e, "some text more text") + self.assertEqual(e, "some text more text") class SplitFieldTests(TestCase): - full_text = u'summary\n\n\n\nmore' - excerpt = u'summary\n' + full_text = 'summary\n\n\n\nmore' + excerpt = 'summary\n' def setUp(self): @@ -57,45 +58,45 @@ class SplitFieldTests(TestCase): def test_unicode_content(self): - self.assertEquals(unicode(self.post.body), self.full_text) + self.assertEqual(text_type(self.post.body), self.full_text) def test_excerpt(self): - self.assertEquals(self.post.body.excerpt, self.excerpt) + self.assertEqual(self.post.body.excerpt, self.excerpt) def test_content(self): - self.assertEquals(self.post.body.content, self.full_text) + self.assertEqual(self.post.body.content, self.full_text) def test_has_more(self): - self.failUnless(self.post.body.has_more) + self.assertTrue(self.post.body.has_more) def test_not_has_more(self): post = Article.objects.create(title='example 2', body='some text\n\nsome more\n') - self.failIf(post.body.has_more) + self.assertFalse(post.body.has_more) def test_load_back(self): post = Article.objects.get(pk=self.post.pk) - self.assertEquals(post.body.content, self.post.body.content) - self.assertEquals(post.body.excerpt, self.post.body.excerpt) + self.assertEqual(post.body.content, self.post.body.content) + self.assertEqual(post.body.excerpt, self.post.body.excerpt) def test_assign_to_body(self): - new_text = u'different\n\n\n\nother' + new_text = 'different\n\n\n\nother' self.post.body = new_text self.post.save() - self.assertEquals(unicode(self.post.body), new_text) + self.assertEqual(text_type(self.post.body), new_text) def test_assign_to_content(self): - new_text = u'different\n\n\n\nother' + new_text = 'different\n\n\n\nother' self.post.body.content = new_text self.post.save() - self.assertEquals(unicode(self.post.body), new_text) + self.assertEqual(text_type(self.post.body), new_text) def test_assign_to_excerpt(self): @@ -112,18 +113,18 @@ class SplitFieldTests(TestCase): def test_none(self): a = Article(title='Some Title', body=None) - self.assertEquals(a.body, None) + self.assertEqual(a.body, None) def test_assign_splittext(self): a = Article(title='Some Title') a.body = self.post.body - self.assertEquals(a.body.excerpt, u'summary\n') + self.assertEqual(a.body.excerpt, 'summary\n') def test_value_to_string(self): f = self.post._meta.get_field('body') - self.assertEquals(f.value_to_string(self.post), self.full_text) + self.assertEqual(f.value_to_string(self.post), self.full_text) def test_abstract_inheritance(self): @@ -144,13 +145,13 @@ class MonitorFieldTests(TestCase): def test_save_no_change(self): self.instance.save() - self.assertEquals(self.instance.name_changed, self.created) + self.assertEqual(self.instance.name_changed, self.created) def test_save_changed(self): self.instance.name = 'Maria' self.instance.save() - self.failUnless(self.instance.name_changed > self.created) + self.assertTrue(self.instance.name_changed > self.created) def test_double_save(self): @@ -158,7 +159,7 @@ class MonitorFieldTests(TestCase): self.instance.save() changed = self.instance.name_changed self.instance.save() - self.assertEquals(self.instance.name_changed, changed) + self.assertEqual(self.instance.name_changed, changed) def test_no_monitor_arg(self): @@ -169,11 +170,11 @@ class StatusFieldTests(TestCase): def test_status_with_default_filled(self): instance = StatusFieldDefaultFilled() - self.assertEquals(instance.status, instance.STATUS.yes) + self.assertEqual(instance.status, instance.STATUS.yes) def test_status_with_default_not_filled(self): instance = StatusFieldDefaultNotFilled() - self.assertEquals(instance.status, instance.STATUS.no) + self.assertEqual(instance.status, instance.STATUS.no) def test_no_check_for_status(self): field = StatusField(no_check_for_status=True) @@ -187,15 +188,15 @@ class ChoicesTests(TestCase): def test_getattr(self): - self.assertEquals(self.STATUS.DRAFT, 'DRAFT') + self.assertEqual(self.STATUS.DRAFT, 'DRAFT') def test_indexing(self): - self.assertEquals(self.STATUS[1], ('PUBLISHED', 'PUBLISHED')) + self.assertEqual(self.STATUS[1], ('PUBLISHED', 'PUBLISHED')) def test_iteration(self): - self.assertEquals(tuple(self.STATUS), (('DRAFT', 'DRAFT'), ('PUBLISHED', 'PUBLISHED'))) + self.assertEqual(tuple(self.STATUS), (('DRAFT', 'DRAFT'), ('PUBLISHED', 'PUBLISHED'))) def test_len(self): @@ -203,10 +204,10 @@ class ChoicesTests(TestCase): def test_repr(self): - self.assertEquals(repr(self.STATUS), - "Choices(" - "('DRAFT', 'DRAFT', 'DRAFT'), " - "('PUBLISHED', 'PUBLISHED', 'PUBLISHED'))") + self.assertEqual(repr(self.STATUS), "Choices" + repr(( + ('DRAFT', 'DRAFT', 'DRAFT'), + ('PUBLISHED', 'PUBLISHED', 'PUBLISHED'), + ))) def test_wrong_length_tuple(self): @@ -224,7 +225,7 @@ class LabelChoicesTests(ChoicesTests): def test_iteration(self): - self.assertEquals(tuple(self.STATUS), ( + self.assertEqual(tuple(self.STATUS), ( ('DRAFT', 'is draft'), ('PUBLISHED', 'is published'), ('DELETED', 'DELETED')) @@ -232,15 +233,15 @@ class LabelChoicesTests(ChoicesTests): def test_indexing(self): - self.assertEquals(self.STATUS[1], ('PUBLISHED', 'is published')) + self.assertEqual(self.STATUS[1], ('PUBLISHED', 'is published')) def test_default(self): - self.assertEquals(self.STATUS.DELETED, 'DELETED') + self.assertEqual(self.STATUS.DELETED, 'DELETED') def test_provided(self): - self.assertEquals(self.STATUS.DRAFT, 'DRAFT') + self.assertEqual(self.STATUS.DRAFT, 'DRAFT') def test_len(self): @@ -248,11 +249,11 @@ class LabelChoicesTests(ChoicesTests): def test_repr(self): - self.assertEquals(repr(self.STATUS), - "Choices(" - "('DRAFT', 'DRAFT', 'is draft'), " - "('PUBLISHED', 'PUBLISHED', 'is published'), " - "('DELETED', 'DELETED', 'DELETED'))") + self.assertEqual(repr(self.STATUS), "Choices" + repr(( + ('DRAFT', 'DRAFT', 'is draft'), + ('PUBLISHED', 'PUBLISHED', 'is published'), + ('DELETED', 'DELETED', 'DELETED'), + ))) @@ -272,11 +273,11 @@ class IdentifierChoicesTests(ChoicesTests): def test_indexing(self): - self.assertEquals(self.STATUS[1], (1, 'is published')) + self.assertEqual(self.STATUS[1], (1, 'is published')) def test_getattr(self): - self.assertEquals(self.STATUS.DRAFT, 0) + self.assertEqual(self.STATUS.DRAFT, 0) def test_len(self): @@ -284,12 +285,11 @@ class IdentifierChoicesTests(ChoicesTests): def test_repr(self): - self.assertEquals(repr(self.STATUS), - "Choices(" - "(0, 'DRAFT', 'is draft'), " - "(1, 'PUBLISHED', 'is published'), " - "(2, 'DELETED', 'is deleted'))") - + self.assertEqual(repr(self.STATUS), "Choices" + repr(( + (0, 'DRAFT', 'is draft'), + (1, 'PUBLISHED', 'is published'), + (2, 'DELETED', 'is deleted'), + ))) class InheritanceManagerTests(TestCase): @@ -309,7 +309,7 @@ class InheritanceManagerTests(TestCase): InheritanceManagerTestParent(pk=self.child2.pk), InheritanceManagerTestParent(pk=self.grandchild1.pk), ]) - self.assertEquals(set(self.get_manager().all()), children) + self.assertEqual(set(self.get_manager().all()), children) def test_select_all_subclasses(self): @@ -318,7 +318,7 @@ class InheritanceManagerTests(TestCase): children.add(self.grandchild1) else: children.add(InheritanceManagerTestChild1(pk=self.grandchild1.pk)) - self.assertEquals( + self.assertEqual( set(self.get_manager().select_subclasses()), children) @@ -328,7 +328,7 @@ class InheritanceManagerTests(TestCase): InheritanceManagerTestParent(pk=self.child2.pk), InheritanceManagerTestChild1(pk=self.grandchild1.pk), ]) - self.assertEquals( + self.assertEqual( set( self.get_manager().select_subclasses( "inheritancemanagertestchild1") @@ -344,7 +344,7 @@ class InheritanceManagerTests(TestCase): InheritanceManagerTestParent(pk=self.child2.pk), self.grandchild1, ]) - self.assertEquals( + self.assertEqual( set( self.get_manager().select_subclasses( "inheritancemanagertestchild1__" @@ -356,7 +356,7 @@ class InheritanceManagerTests(TestCase): def test_get_subclass(self): - self.assertEquals( + self.assertEqual( self.get_manager().get_subclass(pk=self.child1.pk), self.child1) @@ -422,14 +422,14 @@ class TimeStampedModelTests(TestCase): def test_created(self): t1 = TimeStamp.objects.create() t2 = TimeStamp.objects.create() - self.assert_(t2.created > t1.created) + self.assertTrue(t2.created > t1.created) def test_modified(self): t1 = TimeStamp.objects.create() t2 = TimeStamp.objects.create() t1.save() - self.assert_(t2.modified < t1.modified) + self.assertTrue(t2.modified < t1.modified) @@ -440,34 +440,34 @@ class TimeFramedModelTests(TestCase): def test_not_yet_begun(self): TimeFrame.objects.create(start=self.now+timedelta(days=2)) - self.assertEquals(TimeFrame.timeframed.count(), 0) + self.assertEqual(TimeFrame.timeframed.count(), 0) def test_finished(self): TimeFrame.objects.create(end=self.now-timedelta(days=1)) - self.assertEquals(TimeFrame.timeframed.count(), 0) + self.assertEqual(TimeFrame.timeframed.count(), 0) def test_no_end(self): TimeFrame.objects.create(start=self.now-timedelta(days=10)) - self.assertEquals(TimeFrame.timeframed.count(), 1) + self.assertEqual(TimeFrame.timeframed.count(), 1) def test_no_start(self): TimeFrame.objects.create(end=self.now+timedelta(days=2)) - self.assertEquals(TimeFrame.timeframed.count(), 1) + self.assertEqual(TimeFrame.timeframed.count(), 1) def test_within_range(self): TimeFrame.objects.create(start=self.now-timedelta(days=1), end=self.now+timedelta(days=1)) - self.assertEquals(TimeFrame.timeframed.count(), 1) + self.assertEqual(TimeFrame.timeframed.count(), 1) class TimeFrameManagerAddedTests(TestCase): def test_manager_available(self): - self.assert_(isinstance(TimeFrameManagerAdded.timeframed, QueryManager)) + self.assertTrue(isinstance(TimeFrameManagerAdded.timeframed, QueryManager)) def test_conflict_error(self): @@ -488,9 +488,9 @@ class StatusModelTests(TestCase): def test_created(self): c1 = self.model.objects.create() c2 = self.model.objects.create() - self.assert_(c2.status_changed > c1.status_changed) - self.assertEquals(self.model.active.count(), 2) - self.assertEquals(self.model.deleted.count(), 0) + self.assertTrue(c2.status_changed > c1.status_changed) + self.assertEqual(self.model.active.count(), 2) + self.assertEqual(self.model.deleted.count(), 0) def test_modification(self): @@ -498,16 +498,16 @@ class StatusModelTests(TestCase): date_created = t1.status_changed t1.status = self.on_hold t1.save() - self.assertEquals(self.model.active.count(), 0) - self.assertEquals(self.model.on_hold.count(), 1) - self.assert_(t1.status_changed > date_created) + self.assertEqual(self.model.active.count(), 0) + self.assertEqual(self.model.on_hold.count(), 1) + self.assertTrue(t1.status_changed > date_created) date_changed = t1.status_changed t1.save() - self.assertEquals(t1.status_changed, date_changed) + self.assertEqual(t1.status_changed, date_changed) date_active_again = t1.status_changed t1.status = self.active t1.save() - self.assert_(t1.status_changed > date_active_again) + self.assertTrue(t1.status_changed > date_active_again) @@ -521,7 +521,7 @@ class StatusModelPlainTupleTests(StatusModelTests): class StatusManagerAddedTests(TestCase): def test_manager_available(self): - self.assert_(isinstance(StatusManagerAdded.active, QueryManager)) + self.assertTrue(isinstance(StatusManagerAdded.active, QueryManager)) def test_conflict_error(self): @@ -550,17 +550,17 @@ class QueryManagerTests(TestCase): def test_passing_kwargs(self): qs = Post.public.all() - self.assertEquals([p.order for p in qs], [0, 1, 4, 5]) + self.assertEqual([p.order for p in qs], [0, 1, 4, 5]) def test_passing_Q(self): qs = Post.public_confirmed.all() - self.assertEquals([p.order for p in qs], [0, 1]) + self.assertEqual([p.order for p in qs], [0, 1]) def test_ordering(self): qs = Post.public_reversed.all() - self.assertEquals([p.order for p in qs], [5, 4, 1, 0]) + self.assertEqual([p.order for p in qs], [5, 4, 1, 0]) @@ -575,7 +575,7 @@ if introspector: def test_introspector_adds_no_excerpt_field(self): mf = Article._meta.get_field('body') args, kwargs = introspector(mf) - self.assertEquals(kwargs['no_excerpt_field'], 'True') + self.assertEqual(kwargs['no_excerpt_field'], 'True') def test_no_excerpt_field_works(self): @@ -654,7 +654,7 @@ class ModelTrackerTestCase(TestCase): def assertHasChanged(self, **kwargs): tracker = kwargs.pop('tracker', self.tracker) - for field, value in kwargs.iteritems(): + for field, value in kwargs.items(): if value is None: self.assertRaises(FieldError, tracker.has_changed, field) else: @@ -662,7 +662,7 @@ class ModelTrackerTestCase(TestCase): def assertPrevious(self, **kwargs): tracker = kwargs.pop('tracker', self.tracker) - for field, value in kwargs.iteritems(): + for field, value in kwargs.items(): self.assertEqual(tracker.previous(field), value) def assertChanged(self, **kwargs): @@ -674,7 +674,7 @@ class ModelTrackerTestCase(TestCase): self.assertEqual(tracker.current(), kwargs) def update_instance(self, **kwargs): - for field, value in kwargs.iteritems(): + for field, value in kwargs.items(): setattr(self.instance, field, value) self.instance.save() diff --git a/model_utils/tracker.py b/model_utils/tracker.py index d93dfea..a366490 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from django.db import models from django.core.exceptions import FieldError @@ -74,6 +75,6 @@ class ModelInstanceTracker(object): """Returns dict of fields that changed since save (with old values)""" if not self.instance.pk: return {} - saved = self.saved_data.iteritems() + saved = self.saved_data.items() current = self.current() return dict((k, v) for k, v in saved if v != current[k]) diff --git a/setup.py b/setup.py index 9d12670..12af89a 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ setup( author_email='carl@oddbird.net', url='https://github.com/carljm/django-model-utils/', packages=find_packages(), + install_requires=['django>=1.4.2'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', @@ -22,6 +23,11 @@ setup( 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', 'Framework :: Django', ], zip_safe=False, diff --git a/tox.ini b/tox.ini index 55ea176..4449cea 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py26-1.2,py26-1.3,py26-1.4,py26,py26-trunk,py27-1.2,py27-1.3,py27-1.4,py27,py27-trunk,py27-nosouth +envlist=py26-1.4,py26,py26-trunk,py27-1.4,py27,py27-trunk,py27-nosouth,py32-1.5-nosouth,py32-trunk-nosouth,py33-1.5-nosouth,py33-trunk-nosouth [testenv] deps= @@ -8,20 +8,6 @@ deps= coverage==3.6 commands=coverage run -a --branch setup.py test -[testenv:py26-1.2] -basepython=python2.6 -deps= - django==1.2.7 - South==0.7.6 - coverage==3.6 - -[testenv:py26-1.3] -basepython=python2.6 -deps= - django==1.3.7 - South==0.7.6 - coverage==3.6 - [testenv:py26-1.4] basepython=python2.6 deps= @@ -36,20 +22,6 @@ deps= South==0.7.6 coverage==3.6 -[testenv:py27-1.2] -basepython=python2.7 -deps= - django==1.2.7 - South==0.7.6 - coverage==3.6 - -[testenv:py27-1.3] -basepython=python2.7 -deps= - django==1.3.7 - South==0.7.6 - coverage==3.6 - [testenv:py27-1.4] basepython=python2.7 deps= @@ -64,6 +36,30 @@ deps= South==0.7.6 coverage==3.6 +[testenv:py32-1.5-nosouth] +basepython=python3.2 +deps= + django==1.5.0 + coverage==3.6 + +[testenv:py32-trunk-nosouth] +basepython=python3.2 +deps= + https://github.com/django/django/tarball/master + coverage==3.6 + +[testenv:py33-1.5-nosouth] +basepython=python3.3 +deps= + django==1.5.0 + coverage==3.6 + +[testenv:py33-trunk-nosouth] +basepython=python3.3 +deps= + https://github.com/django/django/tarball/master + coverage==3.6 + [testenv:py27-nosouth] deps=