From 69d0985db144a8f173fd2c30035af904f0549025 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 16 Apr 2011 14:51:14 -0500 Subject: [PATCH] Started deprecation for manager_from, InheritanceCastModel, and Django 1.1 support. --- CHANGES.rst | 5 + README.rst | 203 ++++++++++++------------------------- model_utils/__init__.py | 21 ++-- model_utils/managers.py | 5 + model_utils/models.py | 12 +++ model_utils/tests/tests.py | 129 +++++++++++++++++++++-- 6 files changed, 222 insertions(+), 153 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 453dc48..8bde6b2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,11 @@ CHANGES tip (unreleased) ---------------- +- Added pending-deprecation warnings for ``InheritanceCastModel``, + ``manager_from``, and Django 1.1 support. Removed documentation for the + deprecated utilities. Bumped ``ChoiceEnum`` from pending-deprecation to + deprecation. + 0.6.0 (2011.02.18) ------------------ diff --git a/README.rst b/README.rst index ff0131f..e2b32af 100644 --- a/README.rst +++ b/README.rst @@ -24,7 +24,7 @@ your ``INSTALLED_APPS`` setting. Dependencies ------------ -Most of ``django-model-utils`` works with `Django`_ 1.0 or later. +Most of ``django-model-utils`` works with `Django`_ 1.1 or later. `InheritanceManager`_ and `SplitField`_ require Django 1.2 or later. .. _Django: http://www.djangoproject.com/ @@ -272,50 +272,16 @@ default manager for the model. inheritance; it won't work for grandchild models. .. note:: - ``InheritanceManager`` requires Django 1.2 or later. + ``InheritanceManager`` requires Django 1.2 or later. Previous versions of + django-model-utils included ``InheritanceCastModel``, an alternative (and + inferior) approach to this problem that is Django 1.1 + compatible. ``InheritanceCastModel`` will remain in django-model-utils + until support for Django 1.1 is removed, but it is no longer documented and + its use in new code is discouraged. .. _contributed by Jeff Elmore: http://jeffelmore.org/2010/11/11/automatic-downcasting-of-inherited-models-in-django/ -InheritanceCastModel -==================== - -This abstract base class can be inherited by the root (parent) model in a -model-inheritance tree. It solves the same problem as `InheritanceManager`_ in -a way that requires more database queries and is less convenient to use, but is -compatible with Django versions prior to 1.2. Whenever possible, -`InheritanceManager`_ should be used instead. - -Usage:: - - from model_utils.models import InheritanceCastModel - - class Place(InheritanceCastModel): - # ... - - class Restaurant(Place): - # ... - - class Bar(Place): - # ... - - nearby_places = Place.objects.filter(location='here') - for place in nearby_places: - restaurant_or_bar = place.cast() # ... - -This is inefficient for large querysets, as it results in a new query for every -individual returned object. You can use the ``cast()`` method on a queryset to -reduce this to as many queries as subtypes are involved:: - - nearby_places = Place.objects.filter(location='here') - for place in nearby_places.cast(): - # ... - -.. note:: - The ``cast()`` queryset method does *not* return another queryset but an - already evaluated result of the database query. This means that you cannot - chain additional queryset methods after ``cast()``. - TimeStampedModel ================ @@ -348,107 +314,26 @@ set the ordering of the ``QuerySet`` returned by the ``QueryManager`` by chaining a call to ``.order_by()`` on the ``QueryManager`` (this is not required). -manager_from -============ - -A common "gotcha" when defining methods on a custom manager class is -that those same methods are not automatically also available on the -QuerySet used by that model, so are not "chainable". This can be -counterintuitive, as most of the public QuerySet API is also available -on managers. It is possible to create a custom Manager that returns -QuerySets that have the same additional methods, but this requires -boilerplate code. - -The ``manager_from`` function (`created by George Sakkis`_ and -included here by permission) solves this problem with zero -boilerplate. It creates and returns a Manager subclass with additional -behavior defined by mixin subclasses or functions you pass it, and the -returned Manager will return instances of a custom QuerySet with those -same additional methods:: - - from datetime import datetime - from django.db import models - - class AuthorMixin(object): - def by_author(self, user): - return self.filter(user=user) - - class PublishedMixin(object): - def published(self): - return self.filter(published__lte=datetime.now()) - - def unpublished(self): - return self.filter(published__gte=datetime.now()) - - - class Post(models.Model): - user = models.ForeignKey(User) - published = models.DateTimeField() - - objects = manager_from(AuthorMixin, PublishedMixin, unpublished) - - Post.objects.published() - Post.objects.by_author(user=request.user).unpublished() - -.. _created by George Sakkis: http://djangosnippets.org/snippets/2117/ PassThroughManager ================== -The ``PassThroughManager`` class (`contributed by Paul McLanahan`_) solves -the same problem as the above ``manager_from`` function. This class, however, -accomplishes it in a different way. The reason it exists is that the dynamically -generated ``QuerySet`` classes created by the ``manager_from`` function are -not picklable. It's probably not often that a ``QuerySet`` is pickled, but -it is a documented feature of the Django ``QuerySet`` class, and this method -maintains that functionality. +A common "gotcha" when defining methods on a custom manager class is that those +same methods are not automatically also available on the QuerySets returned by +that manager, so are not "chainable". This can be counterintuitive, as most of +the public QuerySet API is mirrored on managers. It is possible to create a +custom Manager that returns QuerySets that have the same additional methods, +but this requires boilerplate code. The ``PassThroughManager`` class +(`contributed by Paul McLanahan`_) removes this boilerplate. -``PassThroughManager`` is a subclass of ``django.db.models.manager.Manager``, -so all that is required is that you change your custom managers to inherit from -``PassThroughManager`` instead of Django's built-in ``Manager`` class. Once you -do this, create your custom ``QuerySet`` class, and have your manager's -``get_query_set`` method return instances of said class, then all of the -methods you add to your custom ``QuerySet`` class will be available from your -manager as well:: +.. _contributed by Paul McLanahan: http://paulm.us/post/3717466639/passthroughmanager-for-django - from datetime import datetime - from django.db import models - from django.db.models.query import QuerySet - - class PostQuerySet(QuerySet): - def by_author(self, user): - return self.filter(user=user) - - def published(self): - return self.filter(published__lte=datetime.now()) - - def unpublished(self): - return self.filter(published__gte=datetime.now()) - - class PostManager(PassThroughManager): - def get_query_set(self): - PostQuerySet(self.model, using=self._db) - - def get_stats(self): - return { - 'published_count': self.published().count(), - 'unpublished_count': self.unpublished().count(), - } - - class Post(models.Model): - user = models.ForeignKey(User) - published = models.DateTimeField() - - objects = PostManager() - - Post.objects.get_stats() - Post.objects.published() - Post.objects.by_author(user=request.user).unpublished() - -Alternatively, if you don't need any methods on your manager that shouldn't also -be on your queryset, a shortcut is available. ``PassThroughManager``'s -constructor takes an optional argument. If you pass it a ``QuerySet`` subclass -it will automatically use that class when creating querysets for the manager:: +To use ``PassThroughManager``, rather than defining a custom manager with +additional methods, define a custom ``QuerySet`` subclass with the additional +methods you want, and pass that ``QuerySet`` subclass to the +``PassThroughManager`` constructor. ``PassThroughManager`` will always return +instances of your custom ``QuerySet``, and you can also call methods of your +custom ``QuerySet`` directly on the manager:: from datetime import datetime from django.db import models @@ -474,5 +359,49 @@ it will automatically use that class when creating querysets for the manager:: Post.objects.published() Post.objects.by_author(user=request.user).unpublished() -.. _contributed by Paul McLanahan: http://paulm.us/post/3717466639/passthroughmanager-for-django +If you want certain methods available only on the manager, or you need to +override other manager methods (particularly ``get_query_set``), you can also +define a custom manager that inherits from ``PassThroughManager``:: + + from datetime import datetime + from django.db import models + from django.db.models.query import QuerySet + + class PostQuerySet(QuerySet): + def by_author(self, user): + return self.filter(user=user) + + def published(self): + return self.filter(published__lte=datetime.now()) + + def unpublished(self): + return self.filter(published__gte=datetime.now()) + + class PostManager(PassThroughManager): + def get_query_set(self): + return PostQuerySet(self.model, using=self._db) + + def get_stats(self): + return { + 'published_count': self.published().count(), + 'unpublished_count': self.unpublished().count(), + } + + class Post(models.Model): + user = models.ForeignKey(User) + published = models.DateTimeField() + + objects = PostManager() + + Post.objects.get_stats() + Post.objects.published() + Post.objects.by_author(user=request.user).unpublished() + +.. note:: + + Previous versions of django-model-utils included ``manager_from``, a + function that solved the same problem as ``PassThroughManager``. The + ``manager_from`` approach created dynamic ``QuerySet`` subclasses on the + fly, which broke pickling of those querysets. For this reason, + ``PassThroughManager`` is recommended instead. diff --git a/model_utils/__init__.py b/model_utils/__init__.py index b9202ce..5ad4572 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,3 +1,12 @@ +from django import VERSION +if VERSION < (1, 2): + import warnings + warnings.warn( + "Django 1.1 support in django-model-utils is pending deprecation.", + PendingDeprecationWarning) + + + class ChoiceEnum(object): """ DEPRECATED: Use ``Choices`` (below) instead. This class has less @@ -5,7 +14,7 @@ class ChoiceEnum(object): surprising data corruption if new choices are inserted in the middle of the list. Automatic assignment of numeric IDs is not such a great idea after all. - + A class to encapsulate handy functionality for lists of choices for a Django model field. @@ -25,12 +34,12 @@ class ChoiceEnum(object): 'PUBLISHED' >>> tuple(STATUS) ((0, 'DRAFT'), (1, 'PUBLISHED')) - + """ def __init__(self, *choices): import warnings warnings.warn("ChoiceEnum is deprecated, use Choices instead.", - PendingDeprecationWarning) + DeprecationWarning) self._choices = tuple(enumerate(choices)) self._choice_dict = dict(self._choices) self._reverse_dict = dict(((i[1], i[0]) for i in self._choices)) @@ -50,7 +59,7 @@ class ChoiceEnum(object): def __repr__(self): return '%s(%s)' % (self.__class__.__name__, ', '.join(("'%s'" % i[1] for i in self._choices))) - + class Choices(object): """ @@ -116,8 +125,8 @@ class Choices(object): try: return self._choice_dict[attname] except KeyError: - raise AttributeError(attname) - + raise AttributeError(attname) + def __getitem__(self, index): return self._choices[index] diff --git a/model_utils/managers.py b/model_utils/managers.py index 132d84a..cf6dcf0 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -1,4 +1,5 @@ from types import ClassType +import warnings from django.contrib.contenttypes.models import ContentType from django.db import models @@ -140,6 +141,10 @@ def manager_from(*mixins, **kwds): (``django.db.models.manager.Manager`` by default). """ + warnings.warn( + "manager_from is pending deprecation; use PassThroughManager instead.", + PendingDeprecationWarning, + stacklevel=2) # collect separately the mixin classes and methods bases = [kwds.get('queryset_cls', QuerySet)] methods = {} diff --git a/model_utils/models.py b/model_utils/models.py index 8025add..3467442 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -1,3 +1,5 @@ +import warnings + from datetime import datetime from django.db import models @@ -18,11 +20,21 @@ class InheritanceCastModel(models.Model): For use in trees of inherited models, to be able to downcast parent instances to their child types. + Pending deprecation; use InheritanceManager instead. + """ real_type = models.ForeignKey(ContentType, editable=False, null=True) objects = manager_from(InheritanceCastMixin) + def __init__(self, *args, **kwargs): + warnings.warn( + "InheritanceCastModel is pending deprecation. " + "Use InheritanceManager instead.", + PendingDeprecationWarning, + stacklevel=2) + super(InheritanceCastModel, self).__init__(*args, **kwargs) + def save(self, *args, **kwargs): if not self.id: self.real_type = self._get_real_type() diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 3d53605..25d97fb 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -1,7 +1,4 @@ -try: - import cPickle as pickle -except ImportError: - import pickle +import pickle, sys, warnings from datetime import datetime, timedelta @@ -25,84 +22,104 @@ from model_utils.tests.models import ( StatusManagerAdded, TimeFrameManagerAdded, Entry, Dude) + class GetExcerptTests(TestCase): def test_split(self): e = get_excerpt("some content\n\n\n\nsome more") self.assertEquals(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') + def test_middle_of_para(self): e = get_excerpt("some text\n\nmore text") self.assertEquals(e, 'some text') + def test_middle_of_line(self): e = get_excerpt("some text more text") self.assertEquals(e, "some text more text") + + class SplitFieldTests(TestCase): full_text = u'summary\n\n\n\nmore' excerpt = u'summary\n' + def setUp(self): self.post = Article.objects.create( title='example post', body=self.full_text) + def test_unicode_content(self): self.assertEquals(unicode(self.post.body), self.full_text) + def test_excerpt(self): self.assertEquals(self.post.body.excerpt, self.excerpt) + def test_content(self): self.assertEquals(self.post.body.content, self.full_text) + def test_has_more(self): self.failUnless(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) + 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) + def test_assign_to_body(self): new_text = u'different\n\n\n\nother' self.post.body = new_text self.post.save() self.assertEquals(unicode(self.post.body), new_text) + def test_assign_to_content(self): new_text = u'different\n\n\n\nother' self.post.body.content = new_text self.post.save() self.assertEquals(unicode(self.post.body), new_text) + def test_assign_to_excerpt(self): def _invalid_assignment(): self.post.body.excerpt = 'this should fail' self.assertRaises(AttributeError, _invalid_assignment) + def test_access_via_class(self): def _invalid_access(): Article.body self.assertRaises(AttributeError, _invalid_access) + def test_none(self): a = Article(title='Some Title', body=None) self.assertEquals(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') + def test_value_to_string(self): f = self.post._meta.get_field('body') self.assertEquals(f.value_to_string(self.post), self.full_text) @@ -113,15 +130,18 @@ class MonitorFieldTests(TestCase): self.instance = Monitored(name='Charlie') self.created = self.instance.name_changed + def test_save_no_change(self): self.instance.save() self.assertEquals(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) + def test_double_save(self): self.instance.name = 'Jose' self.instance.save() @@ -129,33 +149,41 @@ class MonitorFieldTests(TestCase): self.instance.save() self.assertEquals(self.instance.name_changed, changed) + def test_no_monitor_arg(self): self.assertRaises(TypeError, MonitorField) + class ChoicesTests(TestCase): def setUp(self): self.STATUS = Choices('DRAFT', 'PUBLISHED') + def test_getattr(self): self.assertEquals(self.STATUS.DRAFT, 'DRAFT') + def test_indexing(self): self.assertEquals(self.STATUS[1], ('PUBLISHED', 'PUBLISHED')) + def test_iteration(self): self.assertEquals(tuple(self.STATUS), (('DRAFT', 'DRAFT'), ('PUBLISHED', 'PUBLISHED'))) + def test_repr(self): self.assertEquals(repr(self.STATUS), "Choices(" "('DRAFT', 'DRAFT', 'DRAFT'), " "('PUBLISHED', 'PUBLISHED', 'PUBLISHED'))") + def test_wrong_length_tuple(self): self.assertRaises(ValueError, Choices, ('a',)) + class LabelChoicesTests(ChoicesTests): def setUp(self): self.STATUS = Choices( @@ -164,6 +192,7 @@ class LabelChoicesTests(ChoicesTests): 'DELETED', ) + def test_iteration(self): self.assertEquals(tuple(self.STATUS), ( ('DRAFT', 'is draft'), @@ -171,15 +200,19 @@ class LabelChoicesTests(ChoicesTests): ('DELETED', 'DELETED')) ) + def test_indexing(self): self.assertEquals(self.STATUS[1], ('PUBLISHED', 'is published')) + def test_default(self): self.assertEquals(self.STATUS.DELETED, 'DELETED') + def test_provided(self): self.assertEquals(self.STATUS.DRAFT, 'DRAFT') + def test_repr(self): self.assertEquals(repr(self.STATUS), "Choices(" @@ -188,6 +221,7 @@ class LabelChoicesTests(ChoicesTests): "('DELETED', 'DELETED', 'DELETED'))") + class IdentifierChoicesTests(ChoicesTests): def setUp(self): self.STATUS = Choices( @@ -195,18 +229,22 @@ class IdentifierChoicesTests(ChoicesTests): (1, 'PUBLISHED', 'is published'), (2, 'DELETED', 'is deleted')) + def test_iteration(self): self.assertEqual(tuple(self.STATUS), ( (0, 'is draft'), (1, 'is published'), (2, 'is deleted'))) + def test_indexing(self): self.assertEquals(self.STATUS[1], (1, 'is published')) + def test_getattr(self): self.assertEquals(self.STATUS.DRAFT, 0) + def test_repr(self): self.assertEquals(repr(self.STATUS), "Choices(" @@ -220,28 +258,45 @@ class InheritanceCastModelTests(TestCase): self.parent = InheritParent.objects.create() self.child = InheritChild.objects.create() + def test_parent_real_type(self): self.assertEquals(self.parent.real_type, ContentType.objects.get_for_model(InheritParent)) + def test_child_real_type(self): self.assertEquals(self.child.real_type, ContentType.objects.get_for_model(InheritChild)) + def test_cast(self): obj = InheritParent.objects.get(pk=self.child.pk).cast() self.assertEquals(obj.__class__, InheritChild) + # @@@ Use proper test skipping once Django 1.2 is minimum supported version. + if sys.version_info >= (2, 6): + # @@@ catch_warnings only available in Python 2.6 and newer + def test_pending_deprecation(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + InheritParent() + self.assertEqual(len(w), 1) + assert issubclass(w[-1].category, PendingDeprecationWarning) + + + class InheritanceCastQuerysetTests(TestCase): def setUp(self): self.child = InheritChild.objects.create() self.child2 = InheritChild2.objects.create() + def test_cast_manager(self): self.assertEquals(set(InheritParent.objects.cast()), set([self.child, self.child2])) + def test_cast(self): parent = InheritParent.objects.create() obj = InheritParent.objects.filter(pk=self.child.pk).cast()[0] @@ -252,12 +307,15 @@ class InheritanceCastQuerysetTests(TestCase): set([parent, self.child, self.child2])) + +# @@@ Use proper test skipping once 1.2 is minimum supported version. if django.VERSION >= (1, 2): class InheritanceManagerTests(TestCase): def setUp(self): self.child1 = InheritanceManagerTestChild1.objects.create() self.child2 = InheritanceManagerTestChild2.objects.create() + def test_normal(self): self.assertEquals(set(InheritanceManagerTestParent.objects.all()), set([ @@ -265,11 +323,13 @@ if django.VERSION >= (1, 2): InheritanceManagerTestParent(pk=self.child2.pk), ])) + def test_select_all_subclasses(self): self.assertEquals( set(InheritanceManagerTestParent.objects.select_subclasses()), set([self.child1, self.child2])) + def test_select_specific_subclasses(self): self.assertEquals( set(InheritanceManagerTestParent.objects.select_subclasses( @@ -278,12 +338,14 @@ if django.VERSION >= (1, 2): InheritanceManagerTestParent(pk=self.child2.pk)])) + class TimeStampedModelTests(TestCase): def test_created(self): t1 = TimeStamp.objects.create() t2 = TimeStamp.objects.create() self.assert_(t2.created > t1.created) + def test_modified(self): t1 = TimeStamp.objects.create() t2 = TimeStamp.objects.create() @@ -291,37 +353,44 @@ class TimeStampedModelTests(TestCase): self.assert_(t2.modified < t1.modified) -class TimeFramedModelTests(TestCase): +class TimeFramedModelTests(TestCase): def setUp(self): self.now = datetime.now() + def test_not_yet_begun(self): TimeFrame.objects.create(start=self.now+timedelta(days=2)) self.assertEquals(TimeFrame.timeframed.count(), 0) + def test_finished(self): TimeFrame.objects.create(end=self.now-timedelta(days=1)) self.assertEquals(TimeFrame.timeframed.count(), 0) + def test_no_end(self): TimeFrame.objects.create(start=self.now-timedelta(days=10)) self.assertEquals(TimeFrame.timeframed.count(), 1) + def test_no_start(self): TimeFrame.objects.create(end=self.now+timedelta(days=2)) self.assertEquals(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) -class TimeFrameManagerAddedTests(TestCase): + +class TimeFrameManagerAddedTests(TestCase): def test_manager_available(self): self.assert_(isinstance(TimeFrameManagerAdded.timeframed, QueryManager)) + def test_conflict_error(self): def _run(): class ErrorModel(TimeFramedModel): @@ -329,12 +398,14 @@ class TimeFrameManagerAddedTests(TestCase): self.assertRaises(ImproperlyConfigured, _run) + class StatusModelTests(TestCase): def setUp(self): self.model = Status self.on_hold = Status.STATUS.on_hold self.active = Status.STATUS.active + def test_created(self): c1 = self.model.objects.create() c2 = self.model.objects.create() @@ -342,6 +413,7 @@ class StatusModelTests(TestCase): self.assertEquals(self.model.active.count(), 2) self.assertEquals(self.model.deleted.count(), 0) + def test_modification(self): t1 = self.model.objects.create() date_created = t1.status_changed @@ -359,17 +431,20 @@ class StatusModelTests(TestCase): self.assert_(t1.status_changed > date_active_again) + class StatusModelPlainTupleTests(StatusModelTests): def setUp(self): self.model = StatusPlainTuple self.on_hold = StatusPlainTuple.STATUS[2][0] self.active = StatusPlainTuple.STATUS[0][0] -class StatusManagerAddedTests(TestCase): + +class StatusManagerAddedTests(TestCase): def test_manager_available(self): self.assert_(isinstance(StatusManagerAdded.active, QueryManager)) + def test_conflict_error(self): def _run(): class ErrorModel(StatusModel): @@ -381,6 +456,7 @@ class StatusManagerAddedTests(TestCase): self.assertRaises(ImproperlyConfigured, _run) + class QueryManagerTests(TestCase): def setUp(self): data = ((True, True, 0), @@ -392,26 +468,37 @@ class QueryManagerTests(TestCase): for p, c, o in data: Post.objects.create(published=p, confirmed=c, order=o) + def test_passing_kwargs(self): qs = Post.public.all() self.assertEquals([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]) + def test_ordering(self): qs = Post.public_reversed.all() self.assertEquals([p.order for p in qs], [5, 4, 1, 0]) -if 'south' in settings.INSTALLED_APPS: + + +# @@@ Use proper test skipping once Django 1.2 is minimum supported version. +try: + from south.modelsinspector import introspector +except ImportError: + introspector = None + +if introspector is not None: class SouthFreezingTests(TestCase): def test_introspector_adds_no_excerpt_field(self): - from south.modelsinspector import introspector mf = Article._meta.get_field('body') args, kwargs = introspector(mf) self.assertEquals(kwargs['no_excerpt_field'], 'True') + def test_no_excerpt_field_works(self): from models import NoRendered self.assertRaises(FieldDoesNotExist, @@ -419,36 +506,55 @@ if 'south' in settings.INSTALLED_APPS: '_body_excerpt') + class ManagerFromTests(TestCase): def setUp(self): Entry.objects.create(author='George', published=True) Entry.objects.create(author='George', published=False) Entry.objects.create(author='Paul', published=True, feature=True) + def test_chaining(self): self.assertEqual(Entry.objects.by_author('George').published().count(), 1) + def test_function(self): self.assertEqual(Entry.objects.unpublished().count(), 1) + def test_typecheck(self): self.assertRaises(TypeError, manager_from, 'somestring') + def test_custom_get_query_set(self): self.assertEqual(Entry.featured.published().count(), 1) + def test_cant_reconcile_qs_class(self): self.assertRaises(TypeError, Entry.broken.all) + def test_queryset_pickling_fails(self): qs = Entry.objects.all() def dump_load(): pqs = pickle.dumps(qs) - upqs = pickle.loads(pqs) + pickle.loads(pqs) self.assertRaises(pickle.PicklingError, dump_load) + # @@@ Use proper test skipping once Django 1.2 is minimum supported version. + if sys.version_info >= (2, 6): + # @@@ catch_warnings only available in Python 2.6 and newer + def test_pending_deprecation(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + manager_from() + self.assertEqual(len(w), 1) + assert issubclass(w[-1].category, PendingDeprecationWarning) + + + class PassThroughManagerTests(TestCase): def setUp(self): Dude.objects.create(name='The Dude', abides=True, has_rug=False) @@ -456,12 +562,14 @@ class PassThroughManagerTests(TestCase): Dude.objects.create(name='Duder', abides=False, has_rug=False) Dude.objects.create(name='El Duderino', abides=True, has_rug=True) + def test_chaining(self): self.assertEqual(Dude.objects.by_name('Duder').count(), 1) self.assertEqual(Dude.objects.all().by_name('Duder').count(), 1) self.assertEqual(Dude.abiders.rug_positive().count(), 1) self.assertEqual(Dude.abiders.all().rug_positive().count(), 1) + def test_manager_only_methods(self): stats = Dude.abiders.get_stats() self.assertEqual(stats['rug_count'], 1) @@ -469,6 +577,7 @@ class PassThroughManagerTests(TestCase): Dude.abiders.all().get_stats() self.assertRaises(AttributeError, notonqs) + def test_queryset_pickling(self): qs = Dude.objects.all() saltyqs = pickle.dumps(qs)