Commits

Luke Plant  committed 5bd3fe9

Fixes for Python 3.3

  • Participants
  • Parent commits bcf1594

Comments (0)

Files changed (10)

File django_easyfilters/filters.py

+from __future__ import unicode_literals
+
 from datetime import date, timedelta
 from dateutil.relativedelta import relativedelta
 import math
 from django.utils import formats
 from django.utils.dates import MONTHS
 from django.utils.text import capfirst
+import six
+
 from django_easyfilters.queries import date_aggregation, value_counts, numeric_range_counts
 from django_easyfilters.ranges import auto_ranges
+from django_easyfilters.utils import python_2_unicode_compatible
+
+
+if six.PY3:
+    # Support for __cmp__ implementation below
+    def cmp(a, b):
+        return (a > b) - (a < b)
+    from functools import total_ordering
+else:
+    total_ordering = lambda c: c
+
 
 try:
     from collections import namedtuple
     ### Utility methods needed by most/all subclasses ###
 
     def param_from_choice(self, choice):
-        return unicode(choice)
+        return six.text_type(choice)
 
     def paramlist_from_choices(self, choices):
         """
         For a list of choices, return the parameter list that should be created.
         """
-        return map(self.param_from_choice, choices)
+        return list(map(self.param_from_choice, choices))
 
     def build_params(self, add=None, remove=None):
         """
         The choice object could be the 'raw' query string or database value,
         or transformed into something more convenient (e.g. a model instance)
         """
-        return unicode(choice_obj)
+        return six.text_type(choice_obj)
 
 
 class SingleValueMixin(object):
         return obj
 
     def param_from_choice(self, choice):
-        return unicode(choice.pk)
+        return six.text_type(choice.pk)
 
     def get_choices_add(self, qs):
         count_dict = self.get_values_counts(qs)
                 for o in objs]
 
     def param_from_choice(self, choice):
-        return unicode(choice.pk)
+        return six.text_type(choice.pk)
 
     def choices_from_params(self):
         # To create the model instances, we override this method rather than
         return retval
 
 
+@total_ordering
 class DateRangeType(object):
 
     all = {} # Keep a cache, so that we have unique instances
                                          "single" if self.single else "multi",
                                          self.label)
 
+    def __eq__(self, other):
+        return self.__cmp__(other) == 0
+
+    def __lt__(self, other):
+        return self.__cmp__(other) < 0
+
     def __cmp__(self, other):
         if other is None:
             return 1
 DAY         = DateRangeType(3, True,  'day',   _ymd)
 
 
+@python_2_unicode_compatible
+@total_ordering
 class DateChoice(object):
     """
     Represents a choice of date. Params are converted to this, and this is used
         self.range_type = range_type
         self.values = values
 
-    def __unicode__(self):
+    def __str__(self):
         # This is called when converting to URL
         return '..'.join(self.values)
 
     def __repr__(self):
-        return '<DateChoice %s %s>' % (self.range_type, self.__unicode__())
+        return '<DateChoice %s %s>' % (self.range_type, self)
+
+    def __eq__(self, other):
+        return self.__cmp__(other) == 0
+
+    def __lt__(self, other):
+        return self.__cmp__(other) < 0
 
     def __cmp__(self, other):
         # 'greater' means more specific.
             if self.range_type is YEAR:
                 return parts[0]
             elif self.range_type is MONTH:
-                return unicode(MONTHS[int(parts[1])])
+                return six.text_type(MONTHS[int(parts[1])])
             elif self.range_type is DAY:
                 return str(int(parts[-1]))
         else:
         else:
             start, end = self.values
 
-        start_parts = map(int, start.split('-'))
-        end_parts = map(int, end.split('-'))
+        start_parts = list(map(int, start.split('-')))
+        end_parts = list(map(int, end.split('-')))
 
         # Fill the parts we don't have with '1' so that e.g. 2000 becomes
         # 2000-1-1
         self.value, self.inclusive = value, inclusive
 
 
+
 def make_numeric_range_choice(to_python, to_str):
     """
     Returns a Choice class that represents a numeric choice range,
     using the passed in 'to_python' and 'to_str' callables to do
     conversion to/from native data types.
     """
-
+    @python_2_unicode_compatible
+    @total_ordering
     class NumericRangeChoice(object):
         def __init__(self, values):
             # Values are instances of RangeEnd
                 return {field_name + '__gt' + ('e' if start.inclusive else ''): start.value,
                         field_name + '__lt' + ('e' if end.inclusive else ''): end.value}
 
-        def __unicode__(self):
+        def __str__(self):
             return '..'.join([to_str(v.value) + ('i' if v.inclusive else '')
                               for v in self.values])
 
         def __repr__(self):
-            return '<NumericChoice %s>' % self.__unicode__()
+            return '<NumericRangeChoice %s>' % self
+
+
+        def __eq__(self, other):
+            return self.__cmp__(other) == 0
+
+        def __lt__(self, other):
+            return self.__cmp__(other) < 0
 
         def __cmp__(self, other):
             # 'greater' means more specific.

File django_easyfilters/filterset.py

 from django.utils.safestring import mark_safe
 from django.utils.html import escape
 from django.utils.text import capfirst
+import six
 
 from django_easyfilters.filters import FILTER_ADD, FILTER_REMOVE, FILTER_DISPLAY, \
     ValuesFilter, ChoicesFilter, ForeignKeyFilter, ManyToManyFilter, DateTimeFilter, NumericRangeFilter
+from django_easyfilters.utils import python_2_unicode_compatible
 
 
 def non_breaking_spaces(val):
         return res
 
 
+@python_2_unicode_compatible
 class FilterSet(object):
 
     template = u"""
         filters = []
         for i, f in enumerate(self.get_fields()):
             klass = None
-            if isinstance(f, basestring):
+            if isinstance(f, six.string_types):
                 opts = {}
                 field_name = f
             else:
                           for c in self.get_filter_choices(f)
                           if c.link_type == FILTER_REMOVE)
 
-    def __unicode__(self):
+    def __str__(self):
         return self.render()

File django_easyfilters/ranges.py

 from decimal import Decimal, DecimalTuple, ROUND_HALF_EVEN, ROUND_DOWN, ROUND_UP
 import math
 
+from six.moves import xrange
+
 
 def round_dec(d):
-    return d._rescale(0, ROUND_HALF_EVEN)
+    return d.quantize(1, ROUND_HALF_EVEN)
 
 def round_dec_down(d):
-    return d._rescale(0, ROUND_DOWN)
+    return d.quantize(1, ROUND_DOWN)
 
 def round_dec_up(d):
-    return d._rescale(0, ROUND_UP)
+    return d.quantize(1, ROUND_UP)
 
 
 def auto_ranges(lower, upper, max_items):

File django_easyfilters/tests/__init__.py

-from filterset import *
-from ranges import *
+from .filterset import *
+from .ranges import *
 

File django_easyfilters/tests/admin.py

 # admin registration to make it easy to add more data for the test suite.
+from __future__ import unicode_literals
 
-from models import *
+from .models import *
 from django.contrib import admin
 
+from six import text_type
+
 class BookAdmin(admin.ModelAdmin):
     def authors(obj):
-        return ", ".join(unicode(a) for a in obj.authors.all())
+        return ", ".join(text_type(a) for a in obj.authors.all())
     list_display = ["name", authors, "binding", "genre", "price", "date_published", "edition", "rating"]
     list_editable = ["binding", "genre", "price", "date_published", "edition", "rating"]
     list_filter = ["genre", "authors", "binding", "price"]

File django_easyfilters/tests/filterset.py

 from django.http import QueryDict
 from django.test import TestCase
 from django.utils.datastructures import MultiValueDict
+from six import text_type
 
 from django_easyfilters.filterset import FilterSet
 from django_easyfilters.filters import \
     FILTER_ADD, FILTER_REMOVE, FILTER_DISPLAY, \
     ForeignKeyFilter, ValuesFilter, ChoicesFilter, ManyToManyFilter, DateTimeFilter, NumericRangeFilter
 
-from models import Book, Genre, Author, BINDING_CHOICES, Person
+from .models import Book, Genre, Author, BINDING_CHOICES, Person
 
 
 class TestFilterSet(TestCase):
         fs = BookFilterSet(qs, QueryDict(''))
         rendered = fs.render()
         self.assertTrue('Genre' in rendered)
-        self.assertEqual(rendered, unicode(fs))
+        self.assertEqual(rendered, text_type(fs))
 
         # And when in 'already filtered' mode:
         choice = fs.filters[0].get_choices(qs)[0]
             qs_filtered = filter2.apply_filter(qs)
             self.assertEqual(len(qs_filtered), choice.count)
             for book in qs_filtered:
-                self.assertEqual(unicode(book.genre), choice.label)
+                self.assertEqual(text_type(book.genre), choice.label)
         self.assertTrue(reached)
 
     def test_foreignkey_remove_link(self):
         choices = filter1.get_choices(qs)
 
         for choice in choices:
-            count = Book.objects.filter(edition=choice.params.values()[0]).count()
+            count = Book.objects.filter(edition=list(choice.params.values())[0]).count()
             self.assertEqual(choice.count, count)
 
             # Check the filtering
             qs_filtered = filter2.apply_filter(qs)
             self.assertEqual(len(qs_filtered), choice.count)
             for book in qs_filtered:
-                self.assertEqual(unicode(book.edition), choice.label)
+                self.assertEqual(text_type(book.edition), choice.label)
 
             # Check we've got a 'remove link' on filtered.
             choices_filtered = filter2.get_choices(qs)
 
 
         # Check list is full, and in right order
-        self.assertEqual([unicode(v) for v in Book.objects.values_list('edition', flat=True).order_by('edition').distinct()],
+        self.assertEqual([text_type(v) for v in Book.objects.values_list('edition', flat=True).order_by('edition').distinct()],
                          [choice.label for choice in choices])
 
     def test_choices_filter(self):
 
         # Check choice db value in params
         for c in choices:
-            self.assertTrue(c.params.values()[0] in binding_choices_db)
+            self.assertTrue(list(c.params.values())[0] in binding_choices_db)
 
     def test_normalize_choices(self):
         # We shouldn't get links for non-nullable fields when there is only one choice.
         choices = filter1.get_choices(qs)
 
         # Check list is full, and in right order
-        self.assertEqual([unicode(v) for v in Author.objects.all()],
+        self.assertEqual([text_type(v) for v in Author.objects.all()],
                          [choice.label for choice in choices])
 
         for choice in choices:
             author = Author.objects.get(id=param)
 
             # Check the label
-            self.assertEqual(unicode(author),
+            self.assertEqual(text_type(author),
                              choice.label)
 
             # Check the filtering
             choices = filter1.get_choices(qs_emily)
 
         # We should have a 'choices' that includes charlotte and anne
-        self.assertTrue(unicode(anne) in [c.label for c in choices if c.link_type is FILTER_ADD])
-        self.assertTrue(unicode(charlotte) in [c.label for c in choices if c.link_type is FILTER_ADD])
+        self.assertTrue(text_type(anne) in [c.label for c in choices if c.link_type is FILTER_ADD])
+        self.assertTrue(text_type(charlotte) in [c.label for c in choices if c.link_type is FILTER_ADD])
 
         # ... but not emily, because that is obvious and boring
-        self.assertTrue(unicode(emily) not in [c.label for c in choices if c.link_type is FILTER_ADD])
+        self.assertTrue(text_type(emily) not in [c.label for c in choices if c.link_type is FILTER_ADD])
         # emily should be in 'remove' links, however.
-        self.assertTrue(unicode(emily) in [c.label for c in choices if c.link_type is FILTER_REMOVE])
+        self.assertTrue(text_type(emily) in [c.label for c in choices if c.link_type is FILTER_REMOVE])
 
         # Select again - should have sensible params
         anne_choice = [c for c in choices if c.label.startswith('Anne')][0]
-        self.assertTrue(unicode(emily.pk) in anne_choice.params.getlist('authors'))
-        self.assertTrue(unicode(anne.pk) in anne_choice.params.getlist('authors'))
+        self.assertTrue(text_type(emily.pk) in anne_choice.params.getlist('authors'))
+        self.assertTrue(text_type(anne.pk) in anne_choice.params.getlist('authors'))
 
         # Now do the second select:
         filter2 = ManyToManyFilter('authors', Book, anne_choice.params)
         # not adding it (could have books by Emily and Anne, but not Charlotte)
         choices = filter2.get_choices(qs_emily_anne)
         self.assertEqual([(c.label, c.link_type) for c in choices],
-                         [(unicode(emily), FILTER_REMOVE),
-                          (unicode(anne), FILTER_REMOVE),
-                          (unicode(charlotte), FILTER_ADD)])
+                         [(text_type(emily), FILTER_REMOVE),
+                          (text_type(anne), FILTER_REMOVE),
+                          (text_type(charlotte), FILTER_ADD)])
 
     def test_manytomany_filter_invalid_query(self):
         self.do_invalid_query_param_test(lambda params:

File django_easyfilters/tests/models.py

 from django.db import models
 
+from django_easyfilters.utils import python_2_unicode_compatible
 
 BINDING_CHOICES = [
     ('H', 'Hardback'),
     ('C', 'Cloth'),
 ]
 
+@python_2_unicode_compatible
 class Author(models.Model):
     name = models.CharField(max_length=50)
     likes = models.IntegerField(default=0)
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     class Meta:
         ordering = ['name']
 
+
+@python_2_unicode_compatible
 class Genre(models.Model):
     name = models.CharField(max_length=50)
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     class Meta:
         ordering = ['name']
 
+
+@python_2_unicode_compatible
 class Book(models.Model):
     name = models.CharField(max_length=100)
     binding = models.CharField(max_length=2, choices=BINDING_CHOICES)
     edition = models.IntegerField(default=1)
     rating = models.FloatField(null=True)
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
 

File django_easyfilters/utils.py

+import six
+
+# Copied from Django 1.5
+
+def python_2_unicode_compatible(klass):
+    """
+    A decorator that defines __unicode__ and __str__ methods under Python 2.
+    Under Python 3 it does nothing.
+
+    To support Python 2 and 3 with a single code base, define a __str__ method
+    returning text and apply this decorator to the class.
+    """
+    if not six.PY3:
+        klass.__unicode__ = klass.__str__
+        klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
+    return klass

File docs/develop.rst

 
    ./manage.py test django_easyfilters
 
+To run it on all supported platforms, install tox and do::
+
+   tox
+
+
 Editing test fixtures
 ---------------------
 
         "Framework :: Django",
         "Topic :: Software Development :: User Interfaces",
         ],
-    install_requires = ['django >= 1.3', 'python-dateutil'],
+    install_requires = ['django >= 1.3', 'python-dateutil', 'six'],
 )