From 753c4fc4f6f18e9b224f20f61d4ad1fe91248fc8 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 21 Nov 2012 18:03:46 +0100 Subject: [PATCH 01/63] Travis tests for python 3. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ccfdeacbf..77bc53844 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,11 @@ language: python python: - - "2.6" - "2.7" + - "3.3" env: - - DJANGO=https://github.com/django/django/zipball/master + - DJANGO=https://www.djangoproject.com/download/1.5a1/tarball/ - DJANGO=django==1.4.1 --use-mirrors - DJANGO=django==1.3.3 --use-mirrors From 17234a5a3fe4b5db33b79e13d6779889c25c1089 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 21 Nov 2012 18:21:26 +0100 Subject: [PATCH 02/63] Also test 3.2 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 77bc53844..8bec7e0bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: python python: - "2.7" + - "3.2" - "3.3" env: From ab3c47297481b7a4ff66027618f9c05bf02a2204 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 21 Nov 2012 19:36:35 +0100 Subject: [PATCH 03/63] compatible print statements. --- rest_framework/runtests/runtests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/runtests/runtests.py b/rest_framework/runtests/runtests.py index 1bd0a5fc8..138c725bc 100755 --- a/rest_framework/runtests/runtests.py +++ b/rest_framework/runtests/runtests.py @@ -30,7 +30,7 @@ def main(): elif len(sys.argv) == 1: test_case = '' else: - print usage() + print(usage()) sys.exit(1) failures = test_runner.run_tests(['tests' + test_case]) From 91190487b747f0c9c09ecd5678f719bb2e745d27 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 21 Nov 2012 19:42:39 +0100 Subject: [PATCH 04/63] Python 3.3 not available yet. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8bec7e0bb..f30fcef38 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ language: python python: - "2.7" - "3.2" - - "3.3" env: - DJANGO=https://www.djangoproject.com/download/1.5a1/tarball/ From b3698acb6c0b9eaa04189599e27014c788a75adc Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 22 Nov 2012 00:20:49 +0100 Subject: [PATCH 05/63] First passing test under p3k \o/ --- rest_framework/authentication.py | 10 ++- rest_framework/compat.py | 16 ++-- rest_framework/fields.py | 73 +++++++++++-------- rest_framework/mixins.py | 4 +- rest_framework/parsers.py | 8 +- rest_framework/renderers.py | 6 +- rest_framework/request.py | 2 +- rest_framework/settings.py | 4 +- rest_framework/templatetags/rest_framework.py | 16 +++- rest_framework/tests/authentication.py | 2 +- rest_framework/tests/files.py | 3 +- rest_framework/tests/genericrelations.py | 6 +- rest_framework/tests/generics.py | 6 +- rest_framework/tests/parsers.py | 2 +- rest_framework/tests/pk_relations.py | 66 +++++++++-------- rest_framework/tests/renderers.py | 2 +- rest_framework/tests/serializer.py | 28 +++---- rest_framework/tests/views.py | 10 ++- rest_framework/utils/__init__.py | 10 ++- setup.py | 2 +- 20 files changed, 162 insertions(+), 114 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 30c78ebc8..4b18b40c4 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -3,7 +3,11 @@ Provides a set of pluggable authentication policies. """ from django.contrib.auth import authenticate -from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError +from django.utils.encoding import DjangoUnicodeDecodeError +try: + from django.utils.encoding import smart_text +except ImportError: + from django.utils.encoding import smart_unicode as smart_text from rest_framework import exceptions from rest_framework.compat import CsrfViewMiddleware from rest_framework.authtoken.models import Token @@ -41,8 +45,8 @@ class BasicAuthentication(BaseAuthentication): return None try: - userid = smart_unicode(auth_parts[0]) - password = smart_unicode(auth_parts[2]) + userid = smart_text(auth_parts[0]) + password = smart_text(auth_parts[2]) except DjangoUnicodeDecodeError: return None diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 09b763681..dcc8aaa6b 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -3,6 +3,9 @@ The `compat` module provides support for backwards compatibility with older versions of django/python, and compatibility wrappers around optional packages. """ # flake8: noqa +from __future__ import unicode_literals +import six + import django # django-filter is optional @@ -16,7 +19,7 @@ except: try: import cStringIO as StringIO except ImportError: - import StringIO + from six import StringIO def get_concrete_model(model_cls): @@ -38,7 +41,7 @@ else: try: from django.contrib.auth.models import User except ImportError: - raise ImportError(u"User model is not to be found.") + raise ImportError("User model is not to be found.") # First implementation of Django class-based views did not include head method @@ -59,11 +62,11 @@ else: # sanitize keyword arguments for key in initkwargs: if key in cls.http_method_names: - raise TypeError(u"You tried to pass in the %s method name as a " - u"keyword argument to %s(). Don't do that." + raise TypeError("You tried to pass in the %s method name as a " + "keyword argument to %s(). Don't do that." % (key, cls.__name__)) if not hasattr(cls, key): - raise TypeError(u"%s() received an invalid keyword %r" % ( + raise TypeError("%s() received an invalid keyword %r" % ( cls.__name__, key)) def view(request, *args, **kwargs): @@ -130,7 +133,8 @@ else: randrange = random.SystemRandom().randrange else: randrange = random.randrange - _MAX_CSRF_KEY = 18446744073709551616L # 2 << 63 + + _MAX_CSRF_KEY = 18446744073709551616 # 2 << 63 REASON_NO_REFERER = "Referer checking failed - no Referer." REASON_BAD_REFERER = "Referer checking failed - %s does not match %s." diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 25d98645d..42c9a2036 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1,3 +1,7 @@ + +from __future__ import unicode_literals +import six + import copy import datetime import inspect @@ -12,12 +16,19 @@ from django.core.urlresolvers import resolve, get_script_prefix from django.conf import settings from django.forms import widgets from django.forms.models import ModelChoiceIterator -from django.utils.encoding import is_protected_type, smart_unicode +from django.utils.encoding import is_protected_type +try: + from django.utils.encoding import smart_text +except ImportError: + from django.utils.encoding import smart_unicode as smart_text from django.utils.translation import ugettext_lazy as _ from rest_framework.reverse import reverse from rest_framework.compat import parse_date, parse_datetime from rest_framework.compat import timezone -from urlparse import urlparse +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse def is_simple_callable(obj): @@ -92,11 +103,11 @@ class Field(object): if is_protected_type(value): return value - elif hasattr(value, '__iter__') and not isinstance(value, (dict, basestring)): + elif hasattr(value, '__iter__') and not isinstance(value, (dict, six.string_types)): return [self.to_native(item) for item in value] elif isinstance(value, dict): return dict(map(self.to_native, (k, v)) for k, v in value.items()) - return smart_unicode(value) + return smart_text(value) def attributes(self): """ @@ -297,8 +308,8 @@ class RelatedField(WritableField): """ Return a readable representation for use with eg. select widgets. """ - desc = smart_unicode(obj) - ident = smart_unicode(self.to_native(obj)) + desc = smart_text(obj) + ident = smart_text(self.to_native(obj)) if desc == ident: return desc return "%s - %s" % (desc, ident) @@ -401,8 +412,8 @@ class PrimaryKeyRelatedField(RelatedField): """ Return a readable representation for use with eg. select widgets. """ - desc = smart_unicode(obj) - ident = smart_unicode(self.to_native(obj.pk)) + desc = smart_text(obj) + ident = smart_text(self.to_native(obj.pk)) if desc == ident: return desc return "%s - %s" % (desc, ident) @@ -418,7 +429,7 @@ class PrimaryKeyRelatedField(RelatedField): try: return self.queryset.get(pk=data) except ObjectDoesNotExist: - msg = "Invalid pk '%s' - object does not exist." % smart_unicode(data) + msg = "Invalid pk '%s' - object does not exist." % smart_text(data) raise ValidationError(msg) def field_to_native(self, obj, field_name): @@ -446,8 +457,8 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField): """ Return a readable representation for use with eg. select widgets. """ - desc = smart_unicode(obj) - ident = smart_unicode(self.to_native(obj.pk)) + desc = smart_text(obj) + ident = smart_text(self.to_native(obj.pk)) if desc == ident: return desc return "%s - %s" % (desc, ident) @@ -473,7 +484,7 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField): try: return self.queryset.get(pk=data) except ObjectDoesNotExist: - msg = "Invalid pk '%s' - object does not exist." % smart_unicode(data) + msg = "Invalid pk '%s' - object does not exist." % smart_text(data) raise ValidationError(msg) ### Slug relationships @@ -674,7 +685,7 @@ class BooleanField(WritableField): type_name = 'BooleanField' widget = widgets.CheckboxInput default_error_messages = { - 'invalid': _(u"'%s' value must be either True or False."), + 'invalid': _("'%s' value must be either True or False."), } empty = False @@ -713,9 +724,9 @@ class CharField(WritableField): super(CharField, self).validate(value) def from_native(self, value): - if isinstance(value, basestring) or value is None: + if isinstance(value, six.string_types) or value is None: return value - return smart_unicode(value) + return smart_text(value) class URLField(CharField): @@ -773,10 +784,10 @@ class ChoiceField(WritableField): if isinstance(v, (list, tuple)): # This is an optgroup, so look inside the group for options for k2, v2 in v: - if value == smart_unicode(k2): + if value == smart_text(k2): return True else: - if value == smart_unicode(k): + if value == smart_text(k): return True return False @@ -814,7 +825,7 @@ class RegexField(CharField): return self._regex def _set_regex(self, regex): - if isinstance(regex, basestring): + if isinstance(regex, six.string_types): regex = re.compile(regex) self._regex = regex if hasattr(self, '_regex_validator') and self._regex_validator in self.validators: @@ -835,10 +846,10 @@ class DateField(WritableField): type_name = 'DateField' default_error_messages = { - 'invalid': _(u"'%s' value has an invalid date format. It must be " - u"in YYYY-MM-DD format."), - 'invalid_date': _(u"'%s' value has the correct format (YYYY-MM-DD) " - u"but it is an invalid date."), + 'invalid': _("'%s' value has an invalid date format. It must be " + "in YYYY-MM-DD format."), + 'invalid_date': _("'%s' value has the correct format (YYYY-MM-DD) " + "but it is an invalid date."), } empty = None @@ -872,13 +883,13 @@ class DateTimeField(WritableField): type_name = 'DateTimeField' default_error_messages = { - 'invalid': _(u"'%s' value has an invalid format. It must be in " - u"YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."), - 'invalid_date': _(u"'%s' value has the correct format " - u"(YYYY-MM-DD) but it is an invalid date."), - 'invalid_datetime': _(u"'%s' value has the correct format " - u"(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) " - u"but it is an invalid date/time."), + 'invalid': _("'%s' value has an invalid format. It must be in " + "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."), + 'invalid_date': _("'%s' value has the correct format " + "(YYYY-MM-DD) but it is an invalid date."), + 'invalid_datetime': _("'%s' value has the correct format " + "(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) " + "but it is an invalid date/time."), } empty = None @@ -895,8 +906,8 @@ class DateTimeField(WritableField): # local time. This won't work during DST change, but we can't # do much about it, so we let the exceptions percolate up the # call stack. - warnings.warn(u"DateTimeField received a naive datetime (%s)" - u" while time zone support is active." % value, + warnings.warn("DateTimeField received a naive datetime (%s)" + " while time zone support is active." % value, RuntimeWarning) default_timezone = timezone.get_default_timezone() value = timezone.make_aware(value, default_timezone) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 1edcfa5c9..87d97bed4 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -4,6 +4,8 @@ Basic building blocks for generic class based views. We don't bind behaviour to http method handlers yet, which allows mixin classes to be composed in interesting ways. """ +from __future__ import unicode_literals + from django.http import Http404 from rest_framework import status from rest_framework.response import Response @@ -38,7 +40,7 @@ class ListModelMixin(object): List a queryset. Should be mixed in with `MultipleObjectAPIView`. """ - empty_error = u"Empty list and '%(class_name)s.allow_empty' is False." + empty_error = "Empty list and '%(class_name)s.allow_empty' is False." def list(self, request, *args, **kwargs): queryset = self.get_queryset() diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 4841676c9..361dfb77c 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -56,7 +56,7 @@ class JSONParser(BaseParser): """ try: return json.load(stream) - except ValueError, exc: + except ValueError as exc: raise ParseError('JSON parse error - %s' % unicode(exc)) @@ -76,7 +76,7 @@ class YAMLParser(BaseParser): """ try: return yaml.safe_load(stream) - except (ValueError, yaml.parser.ParserError), exc: + except (ValueError, yaml.parser.ParserError) as exc: raise ParseError('YAML parse error - %s' % unicode(exc)) @@ -121,7 +121,7 @@ class MultiPartParser(BaseParser): parser = DjangoMultiPartParser(meta, stream, upload_handlers) data, files = parser.parse() return DataAndFiles(data, files) - except MultiPartParserError, exc: + except MultiPartParserError as exc: raise ParseError('Multipart form parse error - %s' % unicode(exc)) @@ -135,7 +135,7 @@ class XMLParser(BaseParser): def parse(self, stream, media_type=None, parser_context=None): try: tree = ET.parse(stream) - except (ExpatError, ETParseError, ValueError), exc: + except (ExpatError, ETParseError, ValueError) as exc: raise ParseError('XML parse error - %s' % unicode(exc)) data = self._xml_convert(tree.getroot()) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 27340745a..bd0dd663c 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -6,6 +6,8 @@ on the response, such as JSON encoded data or HTML output. REST framework also provides an HTML renderer the renders the browsable API. """ +from __future__ import unicode_literals + import copy import string from django import forms @@ -60,7 +62,7 @@ class JSONRenderer(BaseRenderer): if accepted_media_type: # If the media type looks like 'application/json; indent=4', # then pretty print the result. - base_media_type, params = parse_header(accepted_media_type) + base_media_type, params = parse_header(accepted_media_type.encode('ascii')) indent = params.get('indent', indent) try: indent = max(min(int(indent), 8), 0) @@ -100,7 +102,7 @@ class JSONPRenderer(JSONRenderer): callback = self.get_callback(renderer_context) json = super(JSONPRenderer, self).render(data, accepted_media_type, renderer_context) - return u"%s(%s);" % (callback, json) + return "%s(%s);" % (callback, json) class XMLRenderer(BaseRenderer): diff --git a/rest_framework/request.py b/rest_framework/request.py index a1827ba48..dbe579421 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -9,7 +9,7 @@ The wrapped request then offers a richer API, in particular : - full support of PUT method, including support for file uploads - form overloading of HTTP method, content type and content """ -from StringIO import StringIO +from rest_framework.compat import StringIO from django.http.multipartparser import parse_header from rest_framework import exceptions diff --git a/rest_framework/settings.py b/rest_framework/settings.py index ee24a4ad9..9e73bbfb7 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -19,6 +19,8 @@ back to the defaults. """ from django.conf import settings from django.utils import importlib +from six import string_types + USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None) @@ -98,7 +100,7 @@ def perform_import(val, setting_name): If the given setting is a string import notation, then perform the necessary import or imports. """ - if isinstance(val, basestring): + if isinstance(val, string_types): return import_from_string(val, setting_name) elif isinstance(val, (list, tuple)): return [import_from_string(item, setting_name) for item in val] diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 4e0181ee0..7b9e2c378 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -1,10 +1,18 @@ +from __future__ import unicode_literals + from django import template from django.core.urlresolvers import reverse from django.http import QueryDict -from django.utils.encoding import force_unicode +try: + from django.utils.encoding import force_text +except ImportError: + from django.utils.encoding import force_unicode as force_text from django.utils.html import escape from django.utils.safestring import SafeData, mark_safe -from urlparse import urlsplit, urlunsplit +try: + from urllib.parse import urlsplit, urlunsplit +except ImportError: + from urlparse import urlsplit, urlunsplit import re import string @@ -130,7 +138,7 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru """ trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x safe_input = isinstance(text, SafeData) - words = word_split_re.split(force_unicode(text)) + words = word_split_re.split(force_text(text)) nofollow_attr = nofollow and ' rel="nofollow"' or '' for i, word in enumerate(words): match = None @@ -166,4 +174,4 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru words[i] = mark_safe(word) elif autoescape: words[i] = escape(word) - return mark_safe(u''.join(words)) + return mark_safe(''.join(words)) diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 96ca9f52c..c6b4aedcc 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -44,7 +44,7 @@ class BasicAuthTests(TestCase): def test_post_form_passing_basic_auth(self): """Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF""" - auth = 'Basic %s' % base64.encodestring('%s:%s' % (self.username, self.password)).strip() + auth = b'Basic ' + base64.encodestring(('%s:%s' % (self.username, self.password)).encode('utf8')).strip() response = self.csrf_client.post('/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) diff --git a/rest_framework/tests/files.py b/rest_framework/tests/files.py index 5dd57b7c6..027aecf76 100644 --- a/rest_framework/tests/files.py +++ b/rest_framework/tests/files.py @@ -1,4 +1,5 @@ -import StringIO +from rest_framework.compat import StringIO + import datetime from django.test import TestCase diff --git a/rest_framework/tests/genericrelations.py b/rest_framework/tests/genericrelations.py index bc7378e12..ba29dbed5 100644 --- a/rest_framework/tests/genericrelations.py +++ b/rest_framework/tests/genericrelations.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import * @@ -27,7 +29,7 @@ class TestGenericRelations(TestCase): serializer = BookmarkSerializer(self.bookmark) expected = { - 'tags': [u'django', u'python'], - 'url': u'https://www.djangoproject.com/' + 'tags': ['django', 'python'], + 'url': 'https://www.djangoproject.com/' } self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index a8279ef2b..e4a4db806 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.test import TestCase from django.test.client import RequestFactory from django.utils import simplejson as json @@ -71,7 +73,7 @@ class TestRootView(TestCase): content_type='application/json') response = self.view(request).render() self.assertEquals(response.status_code, status.HTTP_201_CREATED) - self.assertEquals(response.data, {'id': 4, 'text': u'foobar'}) + self.assertEquals(response.data, {'id': 4, 'text': 'foobar'}) created = self.objects.get(id=4) self.assertEquals(created.text, 'foobar') @@ -126,7 +128,7 @@ class TestRootView(TestCase): content_type='application/json') response = self.view(request).render() self.assertEquals(response.status_code, status.HTTP_201_CREATED) - self.assertEquals(response.data, {'id': 4, 'text': u'foobar'}) + self.assertEquals(response.data, {'id': 4, 'text': 'foobar'}) created = self.objects.get(id=4) self.assertEquals(created.text, 'foobar') diff --git a/rest_framework/tests/parsers.py b/rest_framework/tests/parsers.py index 8ab8a52fb..ffa39b1f3 100644 --- a/rest_framework/tests/parsers.py +++ b/rest_framework/tests/parsers.py @@ -131,7 +131,7 @@ # self.assertEqual(data['key1'], 'val1') # self.assertEqual(files['file1'].read(), 'blablabla') -from StringIO import StringIO +from rest_framework.compat import StringIO from django import forms from django.test import TestCase from rest_framework.parsers import FormParser diff --git a/rest_framework/tests/pk_relations.py b/rest_framework/tests/pk_relations.py index 3dcc76f97..cbafa3e03 100644 --- a/rest_framework/tests/pk_relations.py +++ b/rest_framework/tests/pk_relations.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.db import models from django.test import TestCase from rest_framework import serializers @@ -65,9 +67,9 @@ class PrimaryKeyManyToManyTests(TestCase): queryset = ManyToManySource.objects.all() serializer = ManyToManySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'targets': [1]}, - {'id': 2, 'name': u'source-2', 'targets': [1, 2]}, - {'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]} + {'id': 1, 'name': 'source-1', 'targets': [1]}, + {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, + {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]} ] self.assertEquals(serializer.data, expected) @@ -75,14 +77,14 @@ class PrimaryKeyManyToManyTests(TestCase): queryset = ManyToManyTarget.objects.all() serializer = ManyToManyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]}, - {'id': 2, 'name': u'target-2', 'sources': [2, 3]}, - {'id': 3, 'name': u'target-3', 'sources': [3]} + {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, + {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, + {'id': 3, 'name': 'target-3', 'sources': [3]} ] self.assertEquals(serializer.data, expected) def test_many_to_many_update(self): - data = {'id': 1, 'name': u'source-1', 'targets': [1, 2, 3]} + data = {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]} instance = ManyToManySource.objects.get(pk=1) serializer = ManyToManySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -93,14 +95,14 @@ class PrimaryKeyManyToManyTests(TestCase): queryset = ManyToManySource.objects.all() serializer = ManyToManySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'targets': [1, 2, 3]}, - {'id': 2, 'name': u'source-2', 'targets': [1, 2]}, - {'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]} + {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]}, + {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, + {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]} ] self.assertEquals(serializer.data, expected) def test_reverse_many_to_many_update(self): - data = {'id': 1, 'name': u'target-1', 'sources': [1]} + data = {'id': 1, 'name': 'target-1', 'sources': [1]} instance = ManyToManyTarget.objects.get(pk=1) serializer = ManyToManyTargetSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -111,28 +113,28 @@ class PrimaryKeyManyToManyTests(TestCase): queryset = ManyToManyTarget.objects.all() serializer = ManyToManyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': [1]}, - {'id': 2, 'name': u'target-2', 'sources': [2, 3]}, - {'id': 3, 'name': u'target-3', 'sources': [3]} + {'id': 1, 'name': 'target-1', 'sources': [1]}, + {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, + {'id': 3, 'name': 'target-3', 'sources': [3]} ] self.assertEquals(serializer.data, expected) def test_reverse_many_to_many_create(self): - data = {'id': 4, 'name': u'target-4', 'sources': [1, 3]} + data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]} serializer = ManyToManyTargetSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'target-4') + self.assertEqual(obj.name, 'target-4') # Ensure target 4 is added, and everything else is as expected queryset = ManyToManyTarget.objects.all() serializer = ManyToManyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]}, - {'id': 2, 'name': u'target-2', 'sources': [2, 3]}, - {'id': 3, 'name': u'target-3', 'sources': [3]}, - {'id': 4, 'name': u'target-4', 'sources': [1, 3]} + {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, + {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, + {'id': 3, 'name': 'target-3', 'sources': [3]}, + {'id': 4, 'name': 'target-4', 'sources': [1, 3]} ] self.assertEquals(serializer.data, expected) @@ -151,9 +153,9 @@ class PrimaryKeyForeignKeyTests(TestCase): queryset = ForeignKeySource.objects.all() serializer = ForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': 1}, - {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': 1} + {'id': 1, 'name': 'source-1', 'target': 1}, + {'id': 2, 'name': 'source-2', 'target': 1}, + {'id': 3, 'name': 'source-3', 'target': 1} ] self.assertEquals(serializer.data, expected) @@ -161,13 +163,13 @@ class PrimaryKeyForeignKeyTests(TestCase): queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]}, - {'id': 2, 'name': u'target-2', 'sources': []}, + {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, + {'id': 2, 'name': 'target-2', 'sources': []}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_update(self): - data = {'id': 1, 'name': u'source-1', 'target': 2} + data = {'id': 1, 'name': 'source-1', 'target': 2} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -178,9 +180,9 @@ class PrimaryKeyForeignKeyTests(TestCase): queryset = ForeignKeySource.objects.all() serializer = ForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': 2}, - {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': 1} + {'id': 1, 'name': 'source-1', 'target': 2}, + {'id': 2, 'name': 'source-2', 'target': 1}, + {'id': 3, 'name': 'source-3', 'target': 1} ] self.assertEquals(serializer.data, expected) @@ -189,7 +191,7 @@ class PrimaryKeyForeignKeyTests(TestCase): # and cannot be arbitrarily set. # def test_reverse_foreign_key_update(self): - # data = {'id': 1, 'name': u'target-1', 'sources': [1]} + # data = {'id': 1, 'name': 'target-1', 'sources': [1]} # instance = ForeignKeyTarget.objects.get(pk=1) # serializer = ForeignKeyTargetSerializer(instance, data=data) # self.assertTrue(serializer.is_valid()) @@ -200,7 +202,7 @@ class PrimaryKeyForeignKeyTests(TestCase): # queryset = ForeignKeyTarget.objects.all() # serializer = ForeignKeyTargetSerializer(queryset) # expected = [ - # {'id': 1, 'name': u'target-1', 'sources': [1]}, - # {'id': 2, 'name': u'target-2', 'sources': []}, + # {'id': 1, 'name': 'target-1', 'sources': [1]}, + # {'id': 2, 'name': 'target-2', 'sources': []}, # ] # self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index 9be4b1146..a2140361e 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -15,7 +15,7 @@ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ from rest_framework.parsers import YAMLParser, XMLParser from rest_framework.settings import api_settings -from StringIO import StringIO +from rest_framework.compat import StringIO import datetime from decimal import Decimal diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 520029ecd..804f578d4 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import datetime from django.test import TestCase from rest_framework import serializers @@ -48,7 +50,7 @@ class BookSerializer(serializers.ModelSerializer): class ActionItemSerializer(serializers.ModelSerializer): - + class Meta: model = ActionItem @@ -163,12 +165,12 @@ class ValidationTests(TestCase): def test_create(self): serializer = CommentSerializer(data=self.data) self.assertEquals(serializer.is_valid(), False) - self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']}) + self.assertEquals(serializer.errors, {'content': ['Ensure this value has at most 1000 characters (it has 1001).']}) def test_update(self): serializer = CommentSerializer(self.comment, data=self.data) self.assertEquals(serializer.is_valid(), False) - self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']}) + self.assertEquals(serializer.errors, {'content': ['Ensure this value has at most 1000 characters (it has 1001).']}) def test_update_missing_field(self): data = { @@ -177,7 +179,7 @@ class ValidationTests(TestCase): } serializer = CommentSerializer(self.comment, data=data) self.assertEquals(serializer.is_valid(), False) - self.assertEquals(serializer.errors, {'email': [u'This field is required.']}) + self.assertEquals(serializer.errors, {'email': ['This field is required.']}) def test_missing_bool_with_default(self): """Make sure that a boolean value with a 'False' value is not @@ -213,7 +215,7 @@ class ValidationTests(TestCase): serializer = CommentSerializerWithFieldValidator(data=data) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'content': [u'Test not in value']}) + self.assertEquals(serializer.errors, {'content': ['Test not in value']}) def test_cross_field_validation(self): @@ -237,7 +239,7 @@ class ValidationTests(TestCase): serializer = CommentSerializerWithCrossFieldValidator(data=data) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'non_field_errors': [u'Email address not in content']}) + self.assertEquals(serializer.errors, {'non_field_errors': ['Email address not in content']}) def test_null_is_true_fields(self): """ @@ -253,7 +255,7 @@ class ValidationTests(TestCase): } serializer = ActionItemSerializer(data=data) self.assertEquals(serializer.is_valid(), False) - self.assertEquals(serializer.errors, {'title': [u'Ensure this value has at most 200 characters (it has 201).']}) + self.assertEquals(serializer.errors, {'title': ['Ensure this value has at most 200 characters (it has 201).']}) def test_default_modelfield_max_length_exceeded(self): data = { @@ -262,22 +264,22 @@ class ValidationTests(TestCase): } serializer = ActionItemSerializer(data=data) self.assertEquals(serializer.is_valid(), False) - self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']}) + self.assertEquals(serializer.errors, {'info': ['Ensure this value has at most 12 characters (it has 13).']}) class RegexValidationTest(TestCase): def test_create_failed(self): serializer = BookSerializer(data={'isbn': '1234567890'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'isbn': [u'isbn has to be exact 13 numbers']}) + self.assertEquals(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']}) serializer = BookSerializer(data={'isbn': '12345678901234'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'isbn': [u'isbn has to be exact 13 numbers']}) + self.assertEquals(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']}) serializer = BookSerializer(data={'isbn': 'abcdefghijklm'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'isbn': [u'isbn has to be exact 13 numbers']}) + self.assertEquals(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']}) def test_create_success(self): serializer = BookSerializer(data={'isbn': '1234567890123'}) @@ -574,8 +576,8 @@ class SerializerMethodFieldTests(TestCase): serializer = self.serializer_class(source_data) expected = { - 'beep': u'hello!', - 'boop': [u'a', u'b', u'c'], + 'beep': 'hello!', + 'boop': ['a', 'b', 'c'], 'boop_count': 3, } diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py index 43365e07a..e51ca9f3d 100644 --- a/rest_framework/tests/views.py +++ b/rest_framework/tests/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import copy from django.test import TestCase from django.test.client import RequestFactory @@ -47,7 +49,7 @@ class ClassBasedViewIntegrationTests(TestCase): request = factory.post('/', 'f00bar', content_type='application/json') response = self.view(request) expected = { - 'detail': u'JSON parse error - No JSON object could be decoded' + 'detail': 'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEquals(sanitise_json_error(response.data), expected) @@ -62,7 +64,7 @@ class ClassBasedViewIntegrationTests(TestCase): request = factory.post('/', form_data) response = self.view(request) expected = { - 'detail': u'JSON parse error - No JSON object could be decoded' + 'detail': 'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEquals(sanitise_json_error(response.data), expected) @@ -76,7 +78,7 @@ class FunctionBasedViewIntegrationTests(TestCase): request = factory.post('/', 'f00bar', content_type='application/json') response = self.view(request) expected = { - 'detail': u'JSON parse error - No JSON object could be decoded' + 'detail': 'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEquals(sanitise_json_error(response.data), expected) @@ -91,7 +93,7 @@ class FunctionBasedViewIntegrationTests(TestCase): request = factory.post('/', form_data) response = self.view(request) expected = { - 'detail': u'JSON parse error - No JSON object could be decoded' + 'detail': 'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEquals(sanitise_json_error(response.data), expected) diff --git a/rest_framework/utils/__init__.py b/rest_framework/utils/__init__.py index 84fcb5dbb..a2406852d 100644 --- a/rest_framework/utils/__init__.py +++ b/rest_framework/utils/__init__.py @@ -1,4 +1,8 @@ -from django.utils.encoding import smart_unicode + +try: + from django.utils.encoding import smart_text +except ImportError: + from django.utils.encoding import smart_unicode as smart_text from django.utils.xmlutils import SimplerXMLGenerator from rest_framework.compat import StringIO import re @@ -80,10 +84,10 @@ class XMLRenderer(): pass else: - xml.characters(smart_unicode(data)) + xml.characters(smart_text(data)) def dict2xml(self, data): - stream = StringIO.StringIO() + stream = StringIO() xml = SimplerXMLGenerator(stream, "utf-8") xml.startDocument() diff --git a/setup.py b/setup.py index 26d072837..f4f2e5c84 100755 --- a/setup.py +++ b/setup.py @@ -63,7 +63,7 @@ setup( packages=get_packages('rest_framework'), package_data=get_package_data('rest_framework'), test_suite='rest_framework.runtests.runtests.main', - install_requires=[], + install_requires=['six'], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', From 6de938bd705c7fac776c0948baee474bc9fd7c74 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 22 Nov 2012 00:23:21 +0100 Subject: [PATCH 06/63] Don't forget to add six for requirements. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 730c1d07a..092058915 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ Django>=1.3 +six From 80b95438df1c450de62fbea28b26fd91a0bf19d3 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 22 Nov 2012 00:26:10 +0100 Subject: [PATCH 07/63] Don't forget to add six for requirements. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 8bec7e0bb..f97fa36ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ env: install: - pip install $DJANGO - pip install django-filter==0.5.4 --use-mirrors + - pip install six --use-mirrors - export PYTHONPATH=. script: From e9c8af46f18e95d67ca6e9fbe36c66dc8bbb1e6f Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 22 Nov 2012 00:32:00 +0100 Subject: [PATCH 08/63] Fixed test with base64. --- rest_framework/tests/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index c6b4aedcc..90f86feed 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -50,7 +50,7 @@ class BasicAuthTests(TestCase): def test_post_json_passing_basic_auth(self): """Ensure POSTing form over basic auth with correct credentials passes and does not require CSRF""" - auth = 'Basic %s' % base64.encodestring('%s:%s' % (self.username, self.password)).strip() + auth = b'Basic %s' % base64.encodestring(('%s:%s' % (self.username, self.password)).encode('utf8')).strip() response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) From 43c9a1c466a4aab4657419c38451337108e49994 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 22 Nov 2012 00:33:28 +0100 Subject: [PATCH 09/63] Don't test with python 3.3 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f97fa36ca..57b7ef20e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ language: python python: - "2.7" - "3.2" - - "3.3" env: - DJANGO=https://www.djangoproject.com/download/1.5a1/tarball/ From 49f8e6419ad79a27c462eb4b0690f139ab8091de Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 22 Nov 2012 00:43:56 +0100 Subject: [PATCH 10/63] Fixed python2.7 compat issue. --- rest_framework/compat.py | 2 +- rest_framework/tests/files.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index dcc8aaa6b..8c7617c12 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -17,7 +17,7 @@ except: # cStringIO only if it's available, otherwise StringIO try: - import cStringIO as StringIO + import cStringIO.StringIO as StringIO except ImportError: from six import StringIO diff --git a/rest_framework/tests/files.py b/rest_framework/tests/files.py index 027aecf76..e76097063 100644 --- a/rest_framework/tests/files.py +++ b/rest_framework/tests/files.py @@ -29,7 +29,7 @@ class FileSerializerTests(TestCase): def test_create(self): now = datetime.datetime.now() - file = StringIO.StringIO('stuff') + file = StringIO('stuff') file.name = 'stuff.txt' file.size = file.len serializer = UploadedFileSerializer(data={'created': now}, files={'file': file}) From 606c20f012c5a1fdcfd661eb280bab22b94afcf5 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 22 Nov 2012 02:08:00 +0100 Subject: [PATCH 11/63] 6 first tests passes under python 3.2 --- rest_framework/authentication.py | 2 +- rest_framework/request.py | 2 +- rest_framework/tests/authentication.py | 4 ++-- rest_framework/utils/mediatypes.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 4b18b40c4..d283959d1 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -40,7 +40,7 @@ class BasicAuthentication(BaseAuthentication): auth = request.META['HTTP_AUTHORIZATION'].split() if len(auth) == 2 and auth[0].lower() == "basic": try: - auth_parts = base64.b64decode(auth[1]).partition(':') + auth_parts = base64.b64decode(auth[1].encode('utf8')).decode('utf8').partition(':') except TypeError: return None diff --git a/rest_framework/request.py b/rest_framework/request.py index dbe579421..d0c4ded6c 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -20,7 +20,7 @@ def is_form_media_type(media_type): """ Return True if the media type is a valid form media type. """ - base_media_type, params = parse_header(media_type) + base_media_type, params = parse_header(media_type.encode('utf8')) return (base_media_type == 'application/x-www-form-urlencoded' or base_media_type == 'multipart/form-data') diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 90f86feed..b7cf50328 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -44,13 +44,13 @@ class BasicAuthTests(TestCase): def test_post_form_passing_basic_auth(self): """Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF""" - auth = b'Basic ' + base64.encodestring(('%s:%s' % (self.username, self.password)).encode('utf8')).strip() + auth = 'Basic ' + base64.encodestring(('%s:%s' % (self.username, self.password)).encode('utf8')).strip().decode('utf8') response = self.csrf_client.post('/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) def test_post_json_passing_basic_auth(self): """Ensure POSTing form over basic auth with correct credentials passes and does not require CSRF""" - auth = b'Basic %s' % base64.encodestring(('%s:%s' % (self.username, self.password)).encode('utf8')).strip() + auth = 'Basic ' + base64.encodestring(('%s:%s' % (self.username, self.password)).encode('utf8')).strip().decode('utf8') response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py index ee7f3a546..39b4ef931 100644 --- a/rest_framework/utils/mediatypes.py +++ b/rest_framework/utils/mediatypes.py @@ -47,7 +47,7 @@ class _MediaType(object): if media_type_str is None: media_type_str = '' self.orig = media_type_str - self.full_type, self.params = parse_header(media_type_str) + self.full_type, self.params = parse_header(media_type_str.encode('utf8')) self.main_type, sep, self.sub_type = self.full_type.partition('/') def match(self, other): From 4007b56457221f0d80f43c2b5303f11454fd947c Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 22 Nov 2012 08:30:32 +0100 Subject: [PATCH 12/63] 28 tests passes now. --- rest_framework/serializers.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9f4964fae..1163bc053 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1,3 +1,5 @@ +import six + import copy import datetime import types @@ -54,7 +56,7 @@ def _get_declared_fields(bases, attrs): Note that all fields from the base classes are used. """ fields = [(field_name, attrs.pop(field_name)) - for field_name, obj in attrs.items() + for field_name, obj in list(six.iteritems(attrs)) if isinstance(obj, Field)] fields.sort(key=lambda x: x[1].creation_counter) @@ -63,7 +65,7 @@ def _get_declared_fields(bases, attrs): # in order to the correct order of fields. for base in bases[::-1]: if hasattr(base, 'base_fields'): - fields = base.base_fields.items() + fields + fields = list(base.base_fields.items()) + fields return SortedDict(fields) @@ -315,8 +317,8 @@ class BaseSerializer(Field): return self.object -class Serializer(BaseSerializer): - __metaclass__ = SerializerMetaclass +class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)): + pass class ModelSerializerOptions(SerializerOptions): From b68263fb652172c5dd74bb7a1c99f0c1230d76bc Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 23 Nov 2012 01:11:09 +0100 Subject: [PATCH 13/63] Default encoding should probably be latin-1 as some RFC seems to imply it. --- rest_framework/authentication.py | 2 +- rest_framework/tests/authentication.py | 4 ++-- rest_framework/utils/mediatypes.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index d283959d1..15e531bf6 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -40,7 +40,7 @@ class BasicAuthentication(BaseAuthentication): auth = request.META['HTTP_AUTHORIZATION'].split() if len(auth) == 2 and auth[0].lower() == "basic": try: - auth_parts = base64.b64decode(auth[1].encode('utf8')).decode('utf8').partition(':') + auth_parts = base64.b64decode(auth[1].encode('iso-8859-1')).decode('iso-8859-1').partition(':') except TypeError: return None diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index b7cf50328..709058088 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -44,13 +44,13 @@ class BasicAuthTests(TestCase): def test_post_form_passing_basic_auth(self): """Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF""" - auth = 'Basic ' + base64.encodestring(('%s:%s' % (self.username, self.password)).encode('utf8')).strip().decode('utf8') + auth = 'Basic ' + base64.encodestring(('%s:%s' % (self.username, self.password)).encode('iso-8859-1')).strip().decode('iso-8859-1') response = self.csrf_client.post('/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) def test_post_json_passing_basic_auth(self): """Ensure POSTing form over basic auth with correct credentials passes and does not require CSRF""" - auth = 'Basic ' + base64.encodestring(('%s:%s' % (self.username, self.password)).encode('utf8')).strip().decode('utf8') + auth = 'Basic ' + base64.encodestring(('%s:%s' % (self.username, self.password)).encode('iso-8859-1')).strip().decode('iso-8859-1') response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py index 39b4ef931..3fc59eddd 100644 --- a/rest_framework/utils/mediatypes.py +++ b/rest_framework/utils/mediatypes.py @@ -47,7 +47,7 @@ class _MediaType(object): if media_type_str is None: media_type_str = '' self.orig = media_type_str - self.full_type, self.params = parse_header(media_type_str.encode('utf8')) + self.full_type, self.params = parse_header(media_type_str.encode('iso-8859-1')) self.main_type, sep, self.sub_type = self.full_type.partition('/') def match(self, other): From e348ee92552aab51290dfe6b256ad03b8d62e6f9 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 23 Nov 2012 01:12:33 +0100 Subject: [PATCH 14/63] 52 tests passing. Refactored a few string / byte io. --- rest_framework/compat.py | 2 ++ rest_framework/fields.py | 3 +-- rest_framework/parsers.py | 16 ++++++++++------ rest_framework/request.py | 9 +++++---- rest_framework/templatetags/rest_framework.py | 3 ++- rest_framework/tests/files.py | 6 +++--- 6 files changed, 23 insertions(+), 16 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 8c7617c12..6ffada48d 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -21,6 +21,8 @@ try: except ImportError: from six import StringIO +from six import BytesIO + def get_concrete_model(model_cls): try: diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 42c9a2036..5c5a86c1a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -8,8 +8,6 @@ import inspect import re import warnings -from io import BytesIO - from django.core import validators from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.urlresolvers import resolve, get_script_prefix @@ -25,6 +23,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework.reverse import reverse from rest_framework.compat import parse_date, parse_datetime from rest_framework.compat import timezone +from rest_framework.compat import BytesIO try: from urllib.parse import urlparse except ImportError: diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 361dfb77c..d5cfaaf88 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -5,6 +5,8 @@ They give us a generic way of being able to handle various media types on the request, such as form content or json encoded data. """ +import six + from django.http import QueryDict from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError @@ -55,9 +57,10 @@ class JSONParser(BaseParser): `files` will always be `None`. """ try: - return json.load(stream) + data = stream.read().decode('iso-8859-1') + return json.loads(data) except ValueError as exc: - raise ParseError('JSON parse error - %s' % unicode(exc)) + raise ParseError('JSON parse error - %s' % six.text_type(exc)) class YAMLParser(BaseParser): @@ -75,9 +78,10 @@ class YAMLParser(BaseParser): `files` will always be `None`. """ try: - return yaml.safe_load(stream) + data = stream.read().decode('iso-8859-1') + return yaml.safe_load(data) except (ValueError, yaml.parser.ParserError) as exc: - raise ParseError('YAML parse error - %s' % unicode(exc)) + raise ParseError('YAML parse error - %s' % six.u(exc)) class FormParser(BaseParser): @@ -122,7 +126,7 @@ class MultiPartParser(BaseParser): data, files = parser.parse() return DataAndFiles(data, files) except MultiPartParserError as exc: - raise ParseError('Multipart form parse error - %s' % unicode(exc)) + raise ParseError('Multipart form parse error - %s' % six.u(exc)) class XMLParser(BaseParser): @@ -136,7 +140,7 @@ class XMLParser(BaseParser): try: tree = ET.parse(stream) except (ExpatError, ETParseError, ValueError) as exc: - raise ParseError('XML parse error - %s' % unicode(exc)) + raise ParseError('XML parse error - %s' % six.u(exc)) data = self._xml_convert(tree.getroot()) return data diff --git a/rest_framework/request.py b/rest_framework/request.py index d0c4ded6c..05424f211 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -9,7 +9,8 @@ The wrapped request then offers a richer API, in particular : - full support of PUT method, including support for file uploads - form overloading of HTTP method, content type and content """ -from rest_framework.compat import StringIO +import six +from rest_framework.compat import BytesIO from django.http.multipartparser import parse_header from rest_framework import exceptions @@ -20,7 +21,7 @@ def is_form_media_type(media_type): """ Return True if the media type is a valid form media type. """ - base_media_type, params = parse_header(media_type.encode('utf8')) + base_media_type, params = parse_header(media_type.encode('iso-8859-1')) return (base_media_type == 'application/x-www-form-urlencoded' or base_media_type == 'multipart/form-data') @@ -216,7 +217,7 @@ class Request(object): elif hasattr(self._request, 'read'): self._stream = self._request else: - self._stream = StringIO(self.raw_post_data) + self._stream = BytesIO(self.raw_post_data) def _perform_form_overloading(self): """ @@ -251,7 +252,7 @@ class Request(object): self._CONTENT_PARAM in self._data and self._CONTENTTYPE_PARAM in self._data): self._content_type = self._data[self._CONTENTTYPE_PARAM] - self._stream = StringIO(self._data[self._CONTENT_PARAM]) + self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode('iso-8859-1')) self._data, self._files = (Empty, Empty) def _parse(self): diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 7b9e2c378..1fc174fff 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +import six from django import template from django.core.urlresolvers import reverse @@ -104,7 +105,7 @@ def add_class(value, css_class): In the case of REST Framework, the filter is used to add Bootstrap-specific classes to the forms. """ - html = unicode(value) + html = six.text_type(value) match = class_re.search(html) if match: m = re.search(r'^%s$|^%s\s|\s%s\s|\s%s$' % (css_class, css_class, diff --git a/rest_framework/tests/files.py b/rest_framework/tests/files.py index e76097063..a69695ca0 100644 --- a/rest_framework/tests/files.py +++ b/rest_framework/tests/files.py @@ -1,4 +1,4 @@ -from rest_framework.compat import StringIO +from rest_framework.compat import BytesIO import datetime @@ -29,9 +29,9 @@ class FileSerializerTests(TestCase): def test_create(self): now = datetime.datetime.now() - file = StringIO('stuff') + file = BytesIO(b'stuff') file.name = 'stuff.txt' - file.size = file.len + file.size = len(file.getvalue()) serializer = UploadedFileSerializer(data={'created': now}, files={'file': file}) uploaded_file = UploadedFile(file=file, created=now) self.assertTrue(serializer.is_valid()) From 237e35120decb508bbad560b23ceacbcd6fccdf3 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 23 Nov 2012 01:22:39 +0100 Subject: [PATCH 15/63] Exclude python3.2 for django < 1.5 --- .travis.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 57b7ef20e..6acc9fe2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: env: - DJANGO=https://www.djangoproject.com/download/1.5a1/tarball/ - - DJANGO=django==1.4.1 --use-mirrors + - DJANGO=django==1.4.2 --use-mirrors - DJANGO=django==1.3.3 --use-mirrors install: @@ -17,3 +17,10 @@ install: script: - python rest_framework/runtests/runtests.py + +matrix: + exclude: + - python: "3.2" + env: DJANGO=django==1.4.2 --use-mirrors + - python: "3.2" + env: DJANGO=django==1.3.3 --use-mirrors From 17000129e35b10c9d08497a669fd72f8233f065a Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sat, 24 Nov 2012 23:19:03 +0100 Subject: [PATCH 16/63] Every (base) test should now pass with python3. --- rest_framework/authtoken/models.py | 4 ++-- rest_framework/response.py | 6 ++++-- rest_framework/tests/authentication.py | 12 ++++++------ rest_framework/tests/files.py | 3 ++- rest_framework/tests/generics.py | 4 +++- rest_framework/tests/htmlrenderer.py | 10 ++++++---- rest_framework/tests/renderers.py | 16 ++++++++++------ rest_framework/tests/request.py | 12 +++++++----- rest_framework/tests/response.py | 7 ++++--- rest_framework/utils/__init__.py | 5 ++++- 10 files changed, 48 insertions(+), 31 deletions(-) diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 4da2aa625..7f5a75a3d 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -19,8 +19,8 @@ class Token(models.Model): return super(Token, self).save(*args, **kwargs) def generate_key(self): - unique = str(uuid.uuid4()) - return hmac.new(unique, digestmod=sha1).hexdigest() + unique = uuid.uuid4() + return hmac.new(unique.bytes, digestmod=sha1).hexdigest() def __unicode__(self): return self.key diff --git a/rest_framework/response.py b/rest_framework/response.py index be78c43ae..cad95611c 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -1,3 +1,5 @@ +import six + from django.core.handlers.wsgi import STATUS_CODE_TEXT from django.template.response import SimpleTemplateResponse @@ -22,9 +24,9 @@ class Response(SimpleTemplateResponse): self.data = data self.template_name = template_name self.exception = exception - + if headers: - for name,value in headers.iteritems(): + for name, value in six.iteritems(headers): self[name] = value @property diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 709058088..33ef03126 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -157,29 +157,29 @@ class TokenAuthTests(TestCase): def test_token_login_json(self): """Ensure token login view using JSON POST works.""" client = Client(enforce_csrf_checks=True) - response = client.post('/auth-token/login/', + response = client.post('/auth-token/login/', json.dumps({'username': self.username, 'password': self.password}), 'application/json') self.assertEqual(response.status_code, 200) - self.assertEqual(json.loads(response.content)['token'], self.key) + self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key) def test_token_login_json_bad_creds(self): """Ensure token login view using JSON POST fails if bad credentials are used.""" client = Client(enforce_csrf_checks=True) - response = client.post('/auth-token/login/', + response = client.post('/auth-token/login/', json.dumps({'username': self.username, 'password': "badpass"}), 'application/json') self.assertEqual(response.status_code, 400) def test_token_login_json_missing_fields(self): """Ensure token login view using JSON POST fails if missing fields.""" client = Client(enforce_csrf_checks=True) - response = client.post('/auth-token/login/', + response = client.post('/auth-token/login/', json.dumps({'username': self.username}), 'application/json') self.assertEqual(response.status_code, 400) def test_token_login_form(self): """Ensure token login view using form POST works.""" client = Client(enforce_csrf_checks=True) - response = client.post('/auth-token/login/', + response = client.post('/auth-token/login/', {'username': self.username, 'password': self.password}) self.assertEqual(response.status_code, 200) - self.assertEqual(json.loads(response.content)['token'], self.key) + self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key) diff --git a/rest_framework/tests/files.py b/rest_framework/tests/files.py index a69695ca0..42e8ed5fc 100644 --- a/rest_framework/tests/files.py +++ b/rest_framework/tests/files.py @@ -1,6 +1,7 @@ from rest_framework.compat import BytesIO import datetime +import six from django.test import TestCase @@ -29,7 +30,7 @@ class FileSerializerTests(TestCase): def test_create(self): now = datetime.datetime.now() - file = BytesIO(b'stuff') + file = BytesIO(six.b('stuff')) file.name = 'stuff.txt' file.size = len(file.getvalue()) serializer = UploadedFileSerializer(data={'created': now}, files={'file': file}) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index e4a4db806..b6d218473 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import six + from django.test import TestCase from django.test.client import RequestFactory from django.utils import simplejson as json @@ -189,7 +191,7 @@ class TestInstanceView(TestCase): request = factory.delete('/1') response = self.view(request, pk=1).render() self.assertEquals(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEquals(response.content, '') + self.assertEquals(response.content, six.b('')) ids = [obj.id for obj in self.objects.all()] self.assertEquals(ids, [2, 3]) diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py index 4caed59ee..cef3ffe9d 100644 --- a/rest_framework/tests/htmlrenderer.py +++ b/rest_framework/tests/htmlrenderer.py @@ -1,3 +1,5 @@ +import six + from django.core.exceptions import PermissionDenied from django.conf.urls.defaults import patterns, url from django.http import Http404 @@ -68,13 +70,13 @@ class TemplateHTMLRendererTests(TestCase): def test_not_found_html_view(self): response = self.client.get('/not_found') self.assertEquals(response.status_code, 404) - self.assertEquals(response.content, "404 Not Found") + self.assertEquals(response.content, six.b("404 Not Found")) self.assertEquals(response['Content-Type'], 'text/html') def test_permission_denied_html_view(self): response = self.client.get('/permission_denied') self.assertEquals(response.status_code, 403) - self.assertEquals(response.content, "403 Forbidden") + self.assertEquals(response.content, six.b("403 Forbidden")) self.assertEquals(response['Content-Type'], 'text/html') @@ -105,11 +107,11 @@ class TemplateHTMLRendererExceptionTests(TestCase): def test_not_found_html_view_with_template(self): response = self.client.get('/not_found') self.assertEquals(response.status_code, 404) - self.assertEquals(response.content, "404: Not found") + self.assertEquals(response.content, six.b("404: Not found")) self.assertEquals(response['Content-Type'], 'text/html') def test_permission_denied_html_view_with_template(self): response = self.client.get('/permission_denied') self.assertEquals(response.status_code, 403) - self.assertEquals(response.content, "403: Permission denied") + self.assertEquals(response.content, six.b("403: Permission denied")) self.assertEquals(response['Content-Type'], 'text/html') diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index a2140361e..79ace78d6 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -1,5 +1,6 @@ import pickle import re +import six from django.conf.urls.defaults import patterns, url, include from django.core.cache import cache @@ -23,8 +24,8 @@ from decimal import Decimal DUMMYSTATUS = status.HTTP_200_OK DUMMYCONTENT = 'dummycontent' -RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x -RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x +RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii') +RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii') expected_results = [ @@ -141,7 +142,7 @@ class RendererEndToEndTests(TestCase): resp = self.client.head('/') self.assertEquals(resp.status_code, DUMMYSTATUS) self.assertEquals(resp['Content-Type'], RendererA.media_type) - self.assertEquals(resp.content, '') + self.assertEquals(resp.content, six.b('')) def test_default_renderer_serializes_content_on_accept_any(self): """If the Accept header is set to */* the default renderer should serialize the response.""" @@ -268,7 +269,8 @@ class JSONPRendererTests(TestCase): HTTP_ACCEPT='application/javascript') self.assertEquals(resp.status_code, 200) self.assertEquals(resp['Content-Type'], 'application/javascript') - self.assertEquals(resp.content, 'callback(%s);' % _flat_repr) + self.assertEquals(resp.content, + ('callback(%s);' % _flat_repr).encode('ascii')) def test_without_callback_without_json_renderer(self): """ @@ -278,7 +280,8 @@ class JSONPRendererTests(TestCase): HTTP_ACCEPT='application/javascript') self.assertEquals(resp.status_code, 200) self.assertEquals(resp['Content-Type'], 'application/javascript') - self.assertEquals(resp.content, 'callback(%s);' % _flat_repr) + self.assertEquals(resp.content, + ('callback(%s);' % _flat_repr).encode('ascii')) def test_with_callback(self): """ @@ -289,7 +292,8 @@ class JSONPRendererTests(TestCase): HTTP_ACCEPT='application/javascript') self.assertEquals(resp.status_code, 200) self.assertEquals(resp['Content-Type'], 'application/javascript') - self.assertEquals(resp.content, '%s(%s);' % (callback_func, _flat_repr)) + self.assertEquals(resp.content, + ('%s(%s);' % (callback_func, _flat_repr)).encode('ascii')) if yaml: diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index ff48f3fa3..68cfd0299 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -1,6 +1,8 @@ """ Tests for content parsing, and form-overloaded content parsing. """ +import six + from django.conf.urls.defaults import patterns from django.contrib.auth.models import User from django.test import TestCase, Client @@ -78,14 +80,14 @@ class TestContentParsing(TestCase): data = {'qwerty': 'uiop'} request = Request(factory.post('/', data)) request.parsers = (FormParser(), MultiPartParser()) - self.assertEqual(request.DATA.items(), data.items()) + self.assertEqual(list(request.DATA.items()), list(data.items())) def test_request_DATA_with_text_content(self): """ Ensure request.DATA returns content for POST request with non-form content. """ - content = 'qwerty' + content = six.b('qwerty') content_type = 'text/plain' request = Request(factory.post('/', content, content_type=content_type)) request.parsers = (PlainTextParser(),) @@ -98,7 +100,7 @@ class TestContentParsing(TestCase): data = {'qwerty': 'uiop'} request = Request(factory.post('/', data)) request.parsers = (FormParser(), MultiPartParser()) - self.assertEqual(request.POST.items(), data.items()) + self.assertEqual(list(request.POST.items()), list(data.items())) def test_standard_behaviour_determines_form_content_PUT(self): """ @@ -116,14 +118,14 @@ class TestContentParsing(TestCase): request = Request(factory.put('/', data)) request.parsers = (FormParser(), MultiPartParser()) - self.assertEqual(request.DATA.items(), data.items()) + self.assertEqual(list(request.DATA.items()), list(data.items())) def test_standard_behaviour_determines_non_form_content_PUT(self): """ Ensure request.DATA returns content for PUT request with non-form content. """ - content = 'qwerty' + content = six.b('qwerty') content_type = 'text/plain' request = Request(factory.put('/', content, content_type=content_type)) request.parsers = (PlainTextParser(), ) diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py index d7b75450c..237b12a97 100644 --- a/rest_framework/tests/response.py +++ b/rest_framework/tests/response.py @@ -1,4 +1,5 @@ import unittest +import six from django.conf.urls.defaults import patterns, url, include from django.test import TestCase @@ -25,8 +26,8 @@ class MockJsonRenderer(BaseRenderer): DUMMYSTATUS = status.HTTP_200_OK DUMMYCONTENT = 'dummycontent' -RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x -RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x +RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii') +RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii') class RendererA(BaseRenderer): @@ -95,7 +96,7 @@ class RendererIntegrationTests(TestCase): resp = self.client.head('/') self.assertEquals(resp.status_code, DUMMYSTATUS) self.assertEquals(resp['Content-Type'], RendererA.media_type) - self.assertEquals(resp.content, '') + self.assertEquals(resp.content, six.b('')) def test_default_renderer_serializes_content_on_accept_any(self): """If the Accept header is set to */* the default renderer should serialize the response.""" diff --git a/rest_framework/utils/__init__.py b/rest_framework/utils/__init__.py index a2406852d..458793539 100644 --- a/rest_framework/utils/__init__.py +++ b/rest_framework/utils/__init__.py @@ -1,8 +1,11 @@ +import six + try: from django.utils.encoding import smart_text except ImportError: from django.utils.encoding import smart_unicode as smart_text + from django.utils.xmlutils import SimplerXMLGenerator from rest_framework.compat import StringIO import re @@ -74,7 +77,7 @@ class XMLRenderer(): xml.endElement("list-item") elif isinstance(data, dict): - for key, value in data.iteritems(): + for key, value in six.iteritems(data): xml.startElement(key, {}) self._to_xml(xml, value) xml.endElement(key) From bf8ceca12213ade27f7997819ba82a7ba6d2bb99 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 28 Nov 2012 10:52:03 +0100 Subject: [PATCH 17/63] Updated the build to use django 1.5 beta 1. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6acc9fe2a..e4a82b6b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ python: - "3.2" env: - - DJANGO=https://www.djangoproject.com/download/1.5a1/tarball/ + - DJANGO=https://www.djangoproject.com/download/1.5b1/tarball/ - DJANGO=django==1.4.2 --use-mirrors - DJANGO=django==1.3.3 --use-mirrors From 364299b0aa0b36db904d1e6f5f6db324d6e53667 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sun, 2 Dec 2012 01:23:03 +0100 Subject: [PATCH 18/63] py3 compatible setup. --- setup.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index f4f2e5c84..dd017c77d 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +#from __future__ import unicode_literals + from setuptools import setup import re import os @@ -45,9 +47,9 @@ version = get_version('rest_framework') if sys.argv[-1] == 'publish': os.system("python setup.py sdist upload") - print "You probably want to also tag the version now:" - print " git tag -a %s -m 'version %s'" % (version, version) - print " git push --tags" + print("You probably want to also tag the version now:") + print(" git tag -a %s -m 'version %s'" % (version, version)) + print(" git push --tags") sys.exit() From 73572bc199f2542375c89d6e9e9751e06dcfdeec Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sun, 2 Dec 2012 01:23:39 +0100 Subject: [PATCH 19/63] trunk bug. --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index bd0dd663c..8c8f7ead9 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -329,7 +329,7 @@ class BrowsableAPIRenderer(BaseRenderer): } fields = {} - for k, v in serializer.get_fields(True).items(): + for k, v in serializer.get_fields().items(): if getattr(v, 'read_only', True): continue From d6b4a6b04a29f6913f0881099b0ef47a931c64ca Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sun, 2 Dec 2012 01:24:15 +0100 Subject: [PATCH 20/63] Fixed a bug with type and python 2.x compat. --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 8c8f7ead9..4abce9065 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -382,7 +382,7 @@ class BrowsableAPIRenderer(BaseRenderer): # Creating an on the fly form see: # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python - OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) + OnTheFlyForm = type(str("OnTheFlyForm"), (forms.Form,), fields) data = (obj is not None) and serializer.data or None form_instance = OnTheFlyForm(data) return form_instance From fa53dde576c8733292eacf27c80cf7a0ad222c3b Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sun, 2 Dec 2012 01:26:02 +0100 Subject: [PATCH 21/63] Reactivated the python 2.6 tests. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index e4a82b6b0..a9e635803 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python python: + - "2.6" - "2.7" - "3.2" From 1e6927b40d98e2d7be56dcc9385cdc1296be3299 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 2 Jan 2013 16:17:07 +0100 Subject: [PATCH 22/63] Merge remote-tracking branch 'reference/py3k' into p3k --- rest_framework/tests/pk_relations.py | 208 --------------------------- 1 file changed, 208 deletions(-) delete mode 100644 rest_framework/tests/pk_relations.py diff --git a/rest_framework/tests/pk_relations.py b/rest_framework/tests/pk_relations.py deleted file mode 100644 index cbafa3e03..000000000 --- a/rest_framework/tests/pk_relations.py +++ /dev/null @@ -1,208 +0,0 @@ -from __future__ import unicode_literals - -from django.db import models -from django.test import TestCase -from rest_framework import serializers - - -# ManyToMany - -class ManyToManyTarget(models.Model): - name = models.CharField(max_length=100) - - -class ManyToManySource(models.Model): - name = models.CharField(max_length=100) - targets = models.ManyToManyField(ManyToManyTarget, related_name='sources') - - -class ManyToManyTargetSerializer(serializers.ModelSerializer): - sources = serializers.ManyPrimaryKeyRelatedField() - - class Meta: - model = ManyToManyTarget - - -class ManyToManySourceSerializer(serializers.ModelSerializer): - class Meta: - model = ManyToManySource - - -# ForeignKey - -class ForeignKeyTarget(models.Model): - name = models.CharField(max_length=100) - - -class ForeignKeySource(models.Model): - name = models.CharField(max_length=100) - target = models.ForeignKey(ForeignKeyTarget, related_name='sources') - - -class ForeignKeyTargetSerializer(serializers.ModelSerializer): - sources = serializers.ManyPrimaryKeyRelatedField(read_only=True) - - class Meta: - model = ForeignKeyTarget - - -class ForeignKeySourceSerializer(serializers.ModelSerializer): - class Meta: - model = ForeignKeySource - - -# TODO: Add test that .data cannot be accessed prior to .is_valid - -class PrimaryKeyManyToManyTests(TestCase): - def setUp(self): - for idx in range(1, 4): - target = ManyToManyTarget(name='target-%d' % idx) - target.save() - source = ManyToManySource(name='source-%d' % idx) - source.save() - for target in ManyToManyTarget.objects.all(): - source.targets.add(target) - - def test_many_to_many_retrieve(self): - queryset = ManyToManySource.objects.all() - serializer = ManyToManySourceSerializer(queryset) - expected = [ - {'id': 1, 'name': 'source-1', 'targets': [1]}, - {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, - {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]} - ] - self.assertEquals(serializer.data, expected) - - def test_reverse_many_to_many_retrieve(self): - queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset) - expected = [ - {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, - {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, - {'id': 3, 'name': 'target-3', 'sources': [3]} - ] - self.assertEquals(serializer.data, expected) - - def test_many_to_many_update(self): - data = {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]} - instance = ManyToManySource.objects.get(pk=1) - serializer = ManyToManySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - self.assertEquals(serializer.data, data) - serializer.save() - - # Ensure source 1 is updated, and everything else is as expected - queryset = ManyToManySource.objects.all() - serializer = ManyToManySourceSerializer(queryset) - expected = [ - {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]}, - {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, - {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]} - ] - self.assertEquals(serializer.data, expected) - - def test_reverse_many_to_many_update(self): - data = {'id': 1, 'name': 'target-1', 'sources': [1]} - instance = ManyToManyTarget.objects.get(pk=1) - serializer = ManyToManyTargetSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - self.assertEquals(serializer.data, data) - serializer.save() - - # Ensure target 1 is updated, and everything else is as expected - queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset) - expected = [ - {'id': 1, 'name': 'target-1', 'sources': [1]}, - {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, - {'id': 3, 'name': 'target-3', 'sources': [3]} - ] - self.assertEquals(serializer.data, expected) - - def test_reverse_many_to_many_create(self): - data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]} - serializer = ManyToManyTargetSerializer(data=data) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, 'target-4') - - # Ensure target 4 is added, and everything else is as expected - queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset) - expected = [ - {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, - {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, - {'id': 3, 'name': 'target-3', 'sources': [3]}, - {'id': 4, 'name': 'target-4', 'sources': [1, 3]} - ] - self.assertEquals(serializer.data, expected) - - -class PrimaryKeyForeignKeyTests(TestCase): - def setUp(self): - target = ForeignKeyTarget(name='target-1') - target.save() - new_target = ForeignKeyTarget(name='target-2') - new_target.save() - for idx in range(1, 4): - source = ForeignKeySource(name='source-%d' % idx, target=target) - source.save() - - def test_foreign_key_retrieve(self): - queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset) - expected = [ - {'id': 1, 'name': 'source-1', 'target': 1}, - {'id': 2, 'name': 'source-2', 'target': 1}, - {'id': 3, 'name': 'source-3', 'target': 1} - ] - self.assertEquals(serializer.data, expected) - - def test_reverse_foreign_key_retrieve(self): - queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) - expected = [ - {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, - {'id': 2, 'name': 'target-2', 'sources': []}, - ] - self.assertEquals(serializer.data, expected) - - def test_foreign_key_update(self): - data = {'id': 1, 'name': 'source-1', 'target': 2} - instance = ForeignKeySource.objects.get(pk=1) - serializer = ForeignKeySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - self.assertEquals(serializer.data, data) - serializer.save() - - # # Ensure source 1 is updated, and everything else is as expected - queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset) - expected = [ - {'id': 1, 'name': 'source-1', 'target': 2}, - {'id': 2, 'name': 'source-2', 'target': 1}, - {'id': 3, 'name': 'source-3', 'target': 1} - ] - self.assertEquals(serializer.data, expected) - - # reverse foreign keys MUST be read_only - # In the general case they do not provide .remove() or .clear() - # and cannot be arbitrarily set. - - # def test_reverse_foreign_key_update(self): - # data = {'id': 1, 'name': 'target-1', 'sources': [1]} - # instance = ForeignKeyTarget.objects.get(pk=1) - # serializer = ForeignKeyTargetSerializer(instance, data=data) - # self.assertTrue(serializer.is_valid()) - # self.assertEquals(serializer.data, data) - # serializer.save() - - # # Ensure target 1 is updated, and everything else is as expected - # queryset = ForeignKeyTarget.objects.all() - # serializer = ForeignKeyTargetSerializer(queryset) - # expected = [ - # {'id': 1, 'name': 'target-1', 'sources': [1]}, - # {'id': 2, 'name': 'target-2', 'sources': []}, - # ] - # self.assertEquals(serializer.data, expected) From 92f1109cd826a5f22c377ecc037621f564e7978e Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 2 Jan 2013 17:52:31 +0100 Subject: [PATCH 23/63] Exclude non python 3 compatible django verison from the test matrix. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 714634f5d..2eb2ada2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,6 @@ script: matrix: exclude: - python: "3.2" - env: DJANGO=django==1.4.2 --use-mirrors + env: DJANGO=django==1.4.3 --use-mirrors - python: "3.2" - env: DJANGO=django==1.3.3 --use-mirrors + env: DJANGO=django==1.3.5 --use-mirrors From 45d48dd52fda187cbd631d61bdf1bffa834c6ba2 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 2 Jan 2013 18:54:55 +0100 Subject: [PATCH 24/63] urlparse not used here. --- rest_framework/fields.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 23e2ac443..e59cc9b42 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -22,10 +22,6 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import parse_date, parse_datetime from rest_framework.compat import timezone from rest_framework.compat import BytesIO -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse def is_simple_callable(obj): From 9c7524fc33b52e4b119ba65ef9d84a58118dff43 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 2 Jan 2013 19:06:02 +0100 Subject: [PATCH 25/63] Fixed unicode errors. --- rest_framework/tests/relations_hyperlink.py | 164 ++++++++++---------- rest_framework/tests/relations_nested.py | 24 +-- rest_framework/tests/relations_pk.py | 164 ++++++++++---------- rest_framework/tests/serializer.py | 16 +- 4 files changed, 187 insertions(+), 181 deletions(-) diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index 0a7ea0f48..407c04e0e 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.db import models from django.test import TestCase from rest_framework import serializers @@ -93,9 +95,9 @@ class HyperlinkedManyToManyTests(TestCase): queryset = ManyToManySource.objects.all() serializer = ManyToManySourceSerializer(queryset) expected = [ - {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/']}, - {'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, - {'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} + {'url': '/manytomanysource/1/', 'name': 'source-1', 'targets': ['/manytomanytarget/1/']}, + {'url': '/manytomanysource/2/', 'name': 'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, + {'url': '/manytomanysource/3/', 'name': 'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} ] self.assertEquals(serializer.data, expected) @@ -103,14 +105,14 @@ class HyperlinkedManyToManyTests(TestCase): queryset = ManyToManyTarget.objects.all() serializer = ManyToManyTargetSerializer(queryset) expected = [ - {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']}, - {'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, - {'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']} + {'url': '/manytomanytarget/1/', 'name': 'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']}, + {'url': '/manytomanytarget/2/', 'name': 'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, + {'url': '/manytomanytarget/3/', 'name': 'target-3', 'sources': ['/manytomanysource/3/']} ] self.assertEquals(serializer.data, expected) def test_many_to_many_update(self): - data = {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} + data = {'url': '/manytomanysource/1/', 'name': 'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} instance = ManyToManySource.objects.get(pk=1) serializer = ManyToManySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -121,14 +123,14 @@ class HyperlinkedManyToManyTests(TestCase): queryset = ManyToManySource.objects.all() serializer = ManyToManySourceSerializer(queryset) expected = [ - {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}, - {'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, - {'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} + {'url': '/manytomanysource/1/', 'name': 'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}, + {'url': '/manytomanysource/2/', 'name': 'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, + {'url': '/manytomanysource/3/', 'name': 'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} ] self.assertEquals(serializer.data, expected) def test_reverse_many_to_many_update(self): - data = {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/']} + data = {'url': '/manytomanytarget/1/', 'name': 'target-1', 'sources': ['/manytomanysource/1/']} instance = ManyToManyTarget.objects.get(pk=1) serializer = ManyToManyTargetSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -139,48 +141,48 @@ class HyperlinkedManyToManyTests(TestCase): queryset = ManyToManyTarget.objects.all() serializer = ManyToManyTargetSerializer(queryset) expected = [ - {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/']}, - {'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, - {'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']} + {'url': '/manytomanytarget/1/', 'name': 'target-1', 'sources': ['/manytomanysource/1/']}, + {'url': '/manytomanytarget/2/', 'name': 'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, + {'url': '/manytomanytarget/3/', 'name': 'target-3', 'sources': ['/manytomanysource/3/']} ] self.assertEquals(serializer.data, expected) def test_many_to_many_create(self): - data = {'url': '/manytomanysource/4/', 'name': u'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']} + data = {'url': '/manytomanysource/4/', 'name': 'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']} serializer = ManyToManySourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'source-4') + self.assertEqual(obj.name, 'source-4') # Ensure source 4 is added, and everything else is as expected queryset = ManyToManySource.objects.all() serializer = ManyToManySourceSerializer(queryset) expected = [ - {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/']}, - {'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, - {'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}, - {'url': '/manytomanysource/4/', 'name': u'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']} + {'url': '/manytomanysource/1/', 'name': 'source-1', 'targets': ['/manytomanytarget/1/']}, + {'url': '/manytomanysource/2/', 'name': 'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, + {'url': '/manytomanysource/3/', 'name': 'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}, + {'url': '/manytomanysource/4/', 'name': 'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']} ] self.assertEquals(serializer.data, expected) def test_reverse_many_to_many_create(self): - data = {'url': '/manytomanytarget/4/', 'name': u'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']} + data = {'url': '/manytomanytarget/4/', 'name': 'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']} serializer = ManyToManyTargetSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'target-4') + self.assertEqual(obj.name, 'target-4') # Ensure target 4 is added, and everything else is as expected queryset = ManyToManyTarget.objects.all() serializer = ManyToManyTargetSerializer(queryset) expected = [ - {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']}, - {'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, - {'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']}, - {'url': '/manytomanytarget/4/', 'name': u'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']} + {'url': '/manytomanytarget/1/', 'name': 'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']}, + {'url': '/manytomanytarget/2/', 'name': 'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, + {'url': '/manytomanytarget/3/', 'name': 'target-3', 'sources': ['/manytomanysource/3/']}, + {'url': '/manytomanytarget/4/', 'name': 'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']} ] self.assertEquals(serializer.data, expected) @@ -201,9 +203,9 @@ class HyperlinkedForeignKeyTests(TestCase): queryset = ForeignKeySource.objects.all() serializer = ForeignKeySourceSerializer(queryset) expected = [ - {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, - {'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'} + {'url': '/foreignkeysource/1/', 'name': 'source-1', 'target': '/foreignkeytarget/1/'}, + {'url': '/foreignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/foreignkeysource/3/', 'name': 'source-3', 'target': '/foreignkeytarget/1/'} ] self.assertEquals(serializer.data, expected) @@ -211,13 +213,13 @@ class HyperlinkedForeignKeyTests(TestCase): queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) expected = [ - {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']}, - {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []}, + {'url': '/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']}, + {'url': '/foreignkeytarget/2/', 'name': 'target-2', 'sources': []}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_update(self): - data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/2/'} + data = {'url': '/foreignkeysource/1/', 'name': 'source-1', 'target': '/foreignkeytarget/2/'} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -228,14 +230,14 @@ class HyperlinkedForeignKeyTests(TestCase): queryset = ForeignKeySource.objects.all() serializer = ForeignKeySourceSerializer(queryset) expected = [ - {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/2/'}, - {'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'} + {'url': '/foreignkeysource/1/', 'name': 'source-1', 'target': '/foreignkeytarget/2/'}, + {'url': '/foreignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/foreignkeysource/3/', 'name': 'source-3', 'target': '/foreignkeytarget/1/'} ] self.assertEquals(serializer.data, expected) def test_reverse_foreign_key_update(self): - data = {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']} + data = {'url': '/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']} instance = ForeignKeyTarget.objects.get(pk=2) serializer = ForeignKeyTargetSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -244,8 +246,8 @@ class HyperlinkedForeignKeyTests(TestCase): queryset = ForeignKeyTarget.objects.all() new_serializer = ForeignKeyTargetSerializer(queryset) expected = [ - {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']}, - {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []}, + {'url': '/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']}, + {'url': '/foreignkeytarget/2/', 'name': 'target-2', 'sources': []}, ] self.assertEquals(new_serializer.data, expected) @@ -256,54 +258,54 @@ class HyperlinkedForeignKeyTests(TestCase): queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) expected = [ - {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/2/']}, - {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}, + {'url': '/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['/foreignkeysource/2/']}, + {'url': '/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_create(self): - data = {'url': '/foreignkeysource/4/', 'name': u'source-4', 'target': '/foreignkeytarget/2/'} + data = {'url': '/foreignkeysource/4/', 'name': 'source-4', 'target': '/foreignkeytarget/2/'} serializer = ForeignKeySourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'source-4') + self.assertEqual(obj.name, 'source-4') # Ensure source 1 is updated, and everything else is as expected queryset = ForeignKeySource.objects.all() serializer = ForeignKeySourceSerializer(queryset) expected = [ - {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, - {'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}, - {'url': '/foreignkeysource/4/', 'name': u'source-4', 'target': '/foreignkeytarget/2/'}, + {'url': '/foreignkeysource/1/', 'name': 'source-1', 'target': '/foreignkeytarget/1/'}, + {'url': '/foreignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/foreignkeysource/3/', 'name': 'source-3', 'target': '/foreignkeytarget/1/'}, + {'url': '/foreignkeysource/4/', 'name': 'source-4', 'target': '/foreignkeytarget/2/'}, ] self.assertEquals(serializer.data, expected) def test_reverse_foreign_key_create(self): - data = {'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']} + data = {'url': '/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']} serializer = ForeignKeyTargetSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'target-3') + self.assertEqual(obj.name, 'target-3') # Ensure target 4 is added, and everything else is as expected queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) expected = [ - {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/2/']}, - {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []}, - {'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}, + {'url': '/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['/foreignkeysource/2/']}, + {'url': '/foreignkeytarget/2/', 'name': 'target-2', 'sources': []}, + {'url': '/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_update_with_invalid_null(self): - data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': None} + data = {'url': '/foreignkeysource/1/', 'name': 'source-1', 'target': None} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) + self.assertEquals(serializer.errors, {'target': ['Value may not be null']}) class HyperlinkedNullableForeignKeyTests(TestCase): @@ -322,28 +324,28 @@ class HyperlinkedNullableForeignKeyTests(TestCase): queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None}, + {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_create_with_valid_null(self): - data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} + data = {'url': '/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} serializer = NullableForeignKeySourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'source-4') + self.assertEqual(obj.name, 'source-4') # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None}, - {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} + {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, + {'url': '/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} ] self.assertEquals(serializer.data, expected) @@ -352,27 +354,27 @@ class HyperlinkedNullableForeignKeyTests(TestCase): The emptystring should be interpreted as null in the context of relationships. """ - data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': ''} - expected_data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} + data = {'url': '/nullableforeignkeysource/4/', 'name': 'source-4', 'target': ''} + expected_data = {'url': '/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} serializer = NullableForeignKeySourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, expected_data) - self.assertEqual(obj.name, u'source-4') + self.assertEqual(obj.name, 'source-4') # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None}, - {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} + {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, + {'url': '/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} ] self.assertEquals(serializer.data, expected) def test_foreign_key_update_with_valid_null(self): - data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None} + data = {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) serializer = NullableForeignKeySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -383,9 +385,9 @@ class HyperlinkedNullableForeignKeyTests(TestCase): queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None}, - {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None}, + {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}, + {'url': '/nullableforeignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, ] self.assertEquals(serializer.data, expected) @@ -394,8 +396,8 @@ class HyperlinkedNullableForeignKeyTests(TestCase): The emptystring should be interpreted as null in the context of relationships. """ - data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': ''} - expected_data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None} + data = {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': ''} + expected_data = {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) serializer = NullableForeignKeySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -406,9 +408,9 @@ class HyperlinkedNullableForeignKeyTests(TestCase): queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None}, - {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None}, + {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}, + {'url': '/nullableforeignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, ] self.assertEquals(serializer.data, expected) @@ -417,7 +419,7 @@ class HyperlinkedNullableForeignKeyTests(TestCase): # and cannot be arbitrarily set. # def test_reverse_foreign_key_update(self): - # data = {'id': 1, 'name': u'target-1', 'sources': [1]} + # data = {'id': 1, 'name': 'target-1', 'sources': [1]} # instance = ForeignKeyTarget.objects.get(pk=1) # serializer = ForeignKeyTargetSerializer(instance, data=data) # self.assertTrue(serializer.is_valid()) @@ -428,7 +430,7 @@ class HyperlinkedNullableForeignKeyTests(TestCase): # queryset = ForeignKeyTarget.objects.all() # serializer = ForeignKeyTargetSerializer(queryset) # expected = [ - # {'id': 1, 'name': u'target-1', 'sources': [1]}, - # {'id': 2, 'name': u'target-2', 'sources': []}, + # {'id': 1, 'name': 'target-1', 'sources': [1]}, + # {'id': 2, 'name': 'target-2', 'sources': []}, # ] # self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py index b11473780..442cbebeb 100644 --- a/rest_framework/tests/relations_nested.py +++ b/rest_framework/tests/relations_nested.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.db import models from django.test import TestCase from rest_framework import serializers @@ -60,9 +62,9 @@ class ReverseForeignKeyTests(TestCase): queryset = ForeignKeySource.objects.all() serializer = ForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': {'id': 1, 'name': u'target-1'}}, - {'id': 2, 'name': u'source-2', 'target': {'id': 1, 'name': u'target-1'}}, - {'id': 3, 'name': u'source-3', 'target': {'id': 1, 'name': u'target-1'}}, + {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}}, + {'id': 2, 'name': 'source-2', 'target': {'id': 1, 'name': 'target-1'}}, + {'id': 3, 'name': 'source-3', 'target': {'id': 1, 'name': 'target-1'}}, ] self.assertEquals(serializer.data, expected) @@ -70,12 +72,12 @@ class ReverseForeignKeyTests(TestCase): queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': [ - {'id': 1, 'name': u'source-1', 'target': 1}, - {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': 1}, + {'id': 1, 'name': 'target-1', 'sources': [ + {'id': 1, 'name': 'source-1', 'target': 1}, + {'id': 2, 'name': 'source-2', 'target': 1}, + {'id': 3, 'name': 'source-3', 'target': 1}, ]}, - {'id': 2, 'name': u'target-2', 'sources': [ + {'id': 2, 'name': 'target-2', 'sources': [ ]} ] self.assertEquals(serializer.data, expected) @@ -95,8 +97,8 @@ class NestedNullableForeignKeyTests(TestCase): queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': {'id': 1, 'name': u'target-1'}}, - {'id': 2, 'name': u'source-2', 'target': {'id': 1, 'name': u'target-1'}}, - {'id': 3, 'name': u'source-3', 'target': None}, + {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}}, + {'id': 2, 'name': 'source-2', 'target': {'id': 1, 'name': 'target-1'}}, + {'id': 3, 'name': 'source-3', 'target': None}, ] self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index 289670999..a04c5c80e 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.db import models from django.test import TestCase from rest_framework import serializers @@ -78,9 +80,9 @@ class PKManyToManyTests(TestCase): queryset = ManyToManySource.objects.all() serializer = ManyToManySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'targets': [1]}, - {'id': 2, 'name': u'source-2', 'targets': [1, 2]}, - {'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]} + {'id': 1, 'name': 'source-1', 'targets': [1]}, + {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, + {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]} ] self.assertEquals(serializer.data, expected) @@ -88,14 +90,14 @@ class PKManyToManyTests(TestCase): queryset = ManyToManyTarget.objects.all() serializer = ManyToManyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]}, - {'id': 2, 'name': u'target-2', 'sources': [2, 3]}, - {'id': 3, 'name': u'target-3', 'sources': [3]} + {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, + {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, + {'id': 3, 'name': 'target-3', 'sources': [3]} ] self.assertEquals(serializer.data, expected) def test_many_to_many_update(self): - data = {'id': 1, 'name': u'source-1', 'targets': [1, 2, 3]} + data = {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]} instance = ManyToManySource.objects.get(pk=1) serializer = ManyToManySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -106,14 +108,14 @@ class PKManyToManyTests(TestCase): queryset = ManyToManySource.objects.all() serializer = ManyToManySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'targets': [1, 2, 3]}, - {'id': 2, 'name': u'source-2', 'targets': [1, 2]}, - {'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]} + {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]}, + {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, + {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]} ] self.assertEquals(serializer.data, expected) def test_reverse_many_to_many_update(self): - data = {'id': 1, 'name': u'target-1', 'sources': [1]} + data = {'id': 1, 'name': 'target-1', 'sources': [1]} instance = ManyToManyTarget.objects.get(pk=1) serializer = ManyToManyTargetSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -124,47 +126,47 @@ class PKManyToManyTests(TestCase): queryset = ManyToManyTarget.objects.all() serializer = ManyToManyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': [1]}, - {'id': 2, 'name': u'target-2', 'sources': [2, 3]}, - {'id': 3, 'name': u'target-3', 'sources': [3]} + {'id': 1, 'name': 'target-1', 'sources': [1]}, + {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, + {'id': 3, 'name': 'target-3', 'sources': [3]} ] self.assertEquals(serializer.data, expected) def test_many_to_many_create(self): - data = {'id': 4, 'name': u'source-4', 'targets': [1, 3]} + data = {'id': 4, 'name': 'source-4', 'targets': [1, 3]} serializer = ManyToManySourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'source-4') + self.assertEqual(obj.name, 'source-4') # Ensure source 4 is added, and everything else is as expected queryset = ManyToManySource.objects.all() serializer = ManyToManySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'targets': [1]}, - {'id': 2, 'name': u'source-2', 'targets': [1, 2]}, - {'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]}, - {'id': 4, 'name': u'source-4', 'targets': [1, 3]}, + {'id': 1, 'name': 'source-1', 'targets': [1]}, + {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, + {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}, + {'id': 4, 'name': 'source-4', 'targets': [1, 3]}, ] self.assertEquals(serializer.data, expected) def test_reverse_many_to_many_create(self): - data = {'id': 4, 'name': u'target-4', 'sources': [1, 3]} + data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]} serializer = ManyToManyTargetSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'target-4') + self.assertEqual(obj.name, 'target-4') # Ensure target 4 is added, and everything else is as expected queryset = ManyToManyTarget.objects.all() serializer = ManyToManyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]}, - {'id': 2, 'name': u'target-2', 'sources': [2, 3]}, - {'id': 3, 'name': u'target-3', 'sources': [3]}, - {'id': 4, 'name': u'target-4', 'sources': [1, 3]} + {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, + {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, + {'id': 3, 'name': 'target-3', 'sources': [3]}, + {'id': 4, 'name': 'target-4', 'sources': [1, 3]} ] self.assertEquals(serializer.data, expected) @@ -183,9 +185,9 @@ class PKForeignKeyTests(TestCase): queryset = ForeignKeySource.objects.all() serializer = ForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': 1}, - {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': 1} + {'id': 1, 'name': 'source-1', 'target': 1}, + {'id': 2, 'name': 'source-2', 'target': 1}, + {'id': 3, 'name': 'source-3', 'target': 1} ] self.assertEquals(serializer.data, expected) @@ -193,13 +195,13 @@ class PKForeignKeyTests(TestCase): queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]}, - {'id': 2, 'name': u'target-2', 'sources': []}, + {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, + {'id': 2, 'name': 'target-2', 'sources': []}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_update(self): - data = {'id': 1, 'name': u'source-1', 'target': 2} + data = {'id': 1, 'name': 'source-1', 'target': 2} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -210,14 +212,14 @@ class PKForeignKeyTests(TestCase): queryset = ForeignKeySource.objects.all() serializer = ForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': 2}, - {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': 1} + {'id': 1, 'name': 'source-1', 'target': 2}, + {'id': 2, 'name': 'source-2', 'target': 1}, + {'id': 3, 'name': 'source-3', 'target': 1} ] self.assertEquals(serializer.data, expected) def test_reverse_foreign_key_update(self): - data = {'id': 2, 'name': u'target-2', 'sources': [1, 3]} + data = {'id': 2, 'name': 'target-2', 'sources': [1, 3]} instance = ForeignKeyTarget.objects.get(pk=2) serializer = ForeignKeyTargetSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -226,8 +228,8 @@ class PKForeignKeyTests(TestCase): queryset = ForeignKeyTarget.objects.all() new_serializer = ForeignKeyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]}, - {'id': 2, 'name': u'target-2', 'sources': []}, + {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, + {'id': 2, 'name': 'target-2', 'sources': []}, ] self.assertEquals(new_serializer.data, expected) @@ -238,54 +240,54 @@ class PKForeignKeyTests(TestCase): queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': [2]}, - {'id': 2, 'name': u'target-2', 'sources': [1, 3]}, + {'id': 1, 'name': 'target-1', 'sources': [2]}, + {'id': 2, 'name': 'target-2', 'sources': [1, 3]}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_create(self): - data = {'id': 4, 'name': u'source-4', 'target': 2} + data = {'id': 4, 'name': 'source-4', 'target': 2} serializer = ForeignKeySourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'source-4') + self.assertEqual(obj.name, 'source-4') # Ensure source 4 is added, and everything else is as expected queryset = ForeignKeySource.objects.all() serializer = ForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': 1}, - {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': 1}, - {'id': 4, 'name': u'source-4', 'target': 2}, + {'id': 1, 'name': 'source-1', 'target': 1}, + {'id': 2, 'name': 'source-2', 'target': 1}, + {'id': 3, 'name': 'source-3', 'target': 1}, + {'id': 4, 'name': 'source-4', 'target': 2}, ] self.assertEquals(serializer.data, expected) def test_reverse_foreign_key_create(self): - data = {'id': 3, 'name': u'target-3', 'sources': [1, 3]} + data = {'id': 3, 'name': 'target-3', 'sources': [1, 3]} serializer = ForeignKeyTargetSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'target-3') + self.assertEqual(obj.name, 'target-3') # Ensure target 3 is added, and everything else is as expected queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': [2]}, - {'id': 2, 'name': u'target-2', 'sources': []}, - {'id': 3, 'name': u'target-3', 'sources': [1, 3]}, + {'id': 1, 'name': 'target-1', 'sources': [2]}, + {'id': 2, 'name': 'target-2', 'sources': []}, + {'id': 3, 'name': 'target-3', 'sources': [1, 3]}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_update_with_invalid_null(self): - data = {'id': 1, 'name': u'source-1', 'target': None} + data = {'id': 1, 'name': 'source-1', 'target': None} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) + self.assertEquals(serializer.errors, {'target': ['Value may not be null']}) class PKNullableForeignKeyTests(TestCase): @@ -302,28 +304,28 @@ class PKNullableForeignKeyTests(TestCase): queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': 1}, - {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': None}, + {'id': 1, 'name': 'source-1', 'target': 1}, + {'id': 2, 'name': 'source-2', 'target': 1}, + {'id': 3, 'name': 'source-3', 'target': None}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_create_with_valid_null(self): - data = {'id': 4, 'name': u'source-4', 'target': None} + data = {'id': 4, 'name': 'source-4', 'target': None} serializer = NullableForeignKeySourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'source-4') + self.assertEqual(obj.name, 'source-4') # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': 1}, - {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': None}, - {'id': 4, 'name': u'source-4', 'target': None} + {'id': 1, 'name': 'source-1', 'target': 1}, + {'id': 2, 'name': 'source-2', 'target': 1}, + {'id': 3, 'name': 'source-3', 'target': None}, + {'id': 4, 'name': 'source-4', 'target': None} ] self.assertEquals(serializer.data, expected) @@ -332,27 +334,27 @@ class PKNullableForeignKeyTests(TestCase): The emptystring should be interpreted as null in the context of relationships. """ - data = {'id': 4, 'name': u'source-4', 'target': ''} - expected_data = {'id': 4, 'name': u'source-4', 'target': None} + data = {'id': 4, 'name': 'source-4', 'target': ''} + expected_data = {'id': 4, 'name': 'source-4', 'target': None} serializer = NullableForeignKeySourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, expected_data) - self.assertEqual(obj.name, u'source-4') + self.assertEqual(obj.name, 'source-4') # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': 1}, - {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': None}, - {'id': 4, 'name': u'source-4', 'target': None} + {'id': 1, 'name': 'source-1', 'target': 1}, + {'id': 2, 'name': 'source-2', 'target': 1}, + {'id': 3, 'name': 'source-3', 'target': None}, + {'id': 4, 'name': 'source-4', 'target': None} ] self.assertEquals(serializer.data, expected) def test_foreign_key_update_with_valid_null(self): - data = {'id': 1, 'name': u'source-1', 'target': None} + data = {'id': 1, 'name': 'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) serializer = NullableForeignKeySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -363,9 +365,9 @@ class PKNullableForeignKeyTests(TestCase): queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': None}, - {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': None} + {'id': 1, 'name': 'source-1', 'target': None}, + {'id': 2, 'name': 'source-2', 'target': 1}, + {'id': 3, 'name': 'source-3', 'target': None} ] self.assertEquals(serializer.data, expected) @@ -374,8 +376,8 @@ class PKNullableForeignKeyTests(TestCase): The emptystring should be interpreted as null in the context of relationships. """ - data = {'id': 1, 'name': u'source-1', 'target': ''} - expected_data = {'id': 1, 'name': u'source-1', 'target': None} + data = {'id': 1, 'name': 'source-1', 'target': ''} + expected_data = {'id': 1, 'name': 'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) serializer = NullableForeignKeySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -386,9 +388,9 @@ class PKNullableForeignKeyTests(TestCase): queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': None}, - {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': None} + {'id': 1, 'name': 'source-1', 'target': None}, + {'id': 2, 'name': 'source-2', 'target': 1}, + {'id': 3, 'name': 'source-3', 'target': None} ] self.assertEquals(serializer.data, expected) @@ -397,7 +399,7 @@ class PKNullableForeignKeyTests(TestCase): # and cannot be arbitrarily set. # def test_reverse_foreign_key_update(self): - # data = {'id': 1, 'name': u'target-1', 'sources': [1]} + # data = {'id': 1, 'name': 'target-1', 'sources': [1]} # instance = ForeignKeyTarget.objects.get(pk=1) # serializer = ForeignKeyTargetSerializer(instance, data=data) # self.assertTrue(serializer.is_valid()) @@ -408,7 +410,7 @@ class PKNullableForeignKeyTests(TestCase): # queryset = ForeignKeyTarget.objects.all() # serializer = ForeignKeyTargetSerializer(queryset) # expected = [ - # {'id': 1, 'name': u'target-1', 'sources': [1]}, - # {'id': 2, 'name': u'target-2', 'sources': []}, + # {'id': 1, 'name': 'target-1', 'sources': [1]}, + # {'id': 2, 'name': 'target-2', 'sources': []}, # ] # self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 4654882ee..6ce7de31c 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -308,7 +308,7 @@ class ModelValidationTests(TestCase): serializer.save() second_serializer = AlbumsSerializer(data={'title': 'a'}) self.assertFalse(second_serializer.is_valid()) - self.assertEqual(second_serializer.errors, {'title': [u'Album with this Title already exists.']}) + self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']}) def test_foreign_key_with_partial(self): """ @@ -654,11 +654,11 @@ class RelatedTraversalTest(TestCase): serializer = BlogPostSerializer(instance=post) expected = { - 'title': u'Test blog post', + 'title': 'Test blog post', 'comments': [{ - 'text': u'I love this blog post', + 'text': 'I love this blog post', 'post_owner': { - "name": u"django", + "name": "django", "age": None } }] @@ -793,8 +793,8 @@ class DepthTest(TestCase): depth = 1 serializer = BlogPostSerializer(instance=post) - expected = {'id': 1, 'title': u'Test blog post', - 'writer': {'id': 1, 'name': u'django', 'age': 1}} + expected = {'id': 1, 'title': 'Test blog post', + 'writer': {'id': 1, 'name': 'django', 'age': 1}} self.assertEqual(serializer.data, expected) @@ -813,8 +813,8 @@ class DepthTest(TestCase): model = BlogPost serializer = BlogPostSerializer(instance=post) - expected = {'id': 1, 'title': u'Test blog post', - 'writer': {'id': 1, 'name': u'django', 'age': 1}} + expected = {'id': 1, 'title': 'Test blog post', + 'writer': {'id': 1, 'name': 'django', 'age': 1}} self.assertEqual(serializer.data, expected) From c95fa81cb25fbdb7af3c8cc39cc45e49eff66c98 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 2 Jan 2013 19:06:28 +0100 Subject: [PATCH 26/63] Use new exception style --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 91fbe176b..9f35f77cf 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -526,7 +526,7 @@ class ModelSerializer(Serializer): try: instance.full_clean(exclude=self.get_validation_exclusions()) - except ValidationError, err: + except ValidationError as err: self._errors = err.message_dict return None From 4b77b3c5adcc147316629a01e05a3600d1d89d27 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 2 Jan 2013 19:06:55 +0100 Subject: [PATCH 27/63] Move the urlparse lib compatibility to the compat file. --- rest_framework/compat.py | 8 +++++++- rest_framework/relations.py | 4 ++-- rest_framework/templatetags/rest_framework.py | 11 ++++------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index d5a9d9957..42ad9e93e 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -30,6 +30,13 @@ except ImportError: from six import BytesIO +# urlparse compat import (Required because it changed in python 3.x) +try: + from urllib import parse as urlparse +except ImportError: + import urlparse as urlparse + + # Try to import PIL in either of the two ways it can end up installed. try: from PIL import Image @@ -109,7 +116,6 @@ else: import re import random import logging - import urlparse from django.conf import settings from django.core.urlresolvers import get_callable diff --git a/rest_framework/relations.py b/rest_framework/relations.py index fe8cbc446..33d3732f3 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -12,7 +12,7 @@ except ImportError: from django.utils.encoding import smart_unicode as smart_text from rest_framework.fields import Field, WritableField from rest_framework.reverse import reverse -from urlparse import urlparse +from rest_framework.compat import urlparse ##### Relational fields ##### @@ -360,7 +360,7 @@ class HyperlinkedRelatedField(RelatedField): if value.startswith('http:') or value.startswith('https:'): # If needed convert absolute URLs to relative path - value = urlparse(value).path + value = urlparse.urlparse(value).path prefix = get_script_prefix() if value.startswith(prefix): value = '/' + value[len(prefix):] diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 18427802b..52c7a59ce 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals +from __future__ import unicode_literals, absolute_import import six from django import template @@ -10,10 +10,7 @@ except ImportError: from django.utils.encoding import force_unicode as force_text from django.utils.html import escape from django.utils.safestring import SafeData, mark_safe -try: - from urllib.parse import urlsplit, urlunsplit -except ImportError: - from urlparse import urlsplit, urlunsplit +from rest_framework.compat import urlparse import re import string @@ -108,11 +105,11 @@ def replace_query_param(url, key, val): Given a URL and a key/val pair, set or replace an item in the query parameters of the URL, and return the new URL. """ - (scheme, netloc, path, query, fragment) = urlsplit(url) + (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url) query_dict = QueryDict(query).copy() query_dict[key] = val query = query_dict.urlencode() - return urlunsplit((scheme, netloc, path, query, fragment)) + return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) # Regex for adding classes to html snippets From 22b1411f413fdfee6e654d1578db69df3c300602 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 3 Jan 2013 10:47:24 +0100 Subject: [PATCH 28/63] By default, don't install six. --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 092058915..730c1d07a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ Django>=1.3 -six From cf51dcc9bb409fb985d5aa09c426d1ed33f6e9b4 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 3 Jan 2013 10:48:43 +0100 Subject: [PATCH 29/63] Straight import is enough. --- rest_framework/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 42ad9e93e..9b38c2084 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -34,7 +34,7 @@ from six import BytesIO try: from urllib import parse as urlparse except ImportError: - import urlparse as urlparse + import urlparse # Try to import PIL in either of the two ways it can end up installed. From 60250f22c8e144494f372338c16a2167cccb319d Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 3 Jan 2013 11:41:07 +0100 Subject: [PATCH 30/63] Move the various compat things to the compat module. --- rest_framework/compat.py | 24 ++++++++++++++++--- rest_framework/fields.py | 7 ++---- rest_framework/parsers.py | 3 +-- rest_framework/relations.py | 5 +--- rest_framework/request.py | 1 - rest_framework/response.py | 4 ++-- rest_framework/serializers.py | 3 +-- rest_framework/settings.py | 6 ++--- rest_framework/templatetags/rest_framework.py | 8 +++---- rest_framework/tests/files.py | 5 ++-- rest_framework/tests/generics.py | 4 +--- rest_framework/tests/htmlrenderer.py | 3 +-- rest_framework/tests/renderers.py | 2 +- rest_framework/tests/request.py | 3 +-- rest_framework/tests/response.py | 4 +--- rest_framework/utils/__init__.py | 10 ++------ 16 files changed, 43 insertions(+), 49 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 9b38c2084..5924cd6d3 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -4,16 +4,34 @@ versions of django/python, and compatibility wrappers around optional packages. """ # flake8: noqa from __future__ import unicode_literals -import six import django +# Try to import six from Django, fallback to six itself (1.3.x) +try: + from django.utils import six +except: + import six + # location of patterns, url, include changes in 1.4 onwards try: from django.conf.urls import patterns, url, include except: from django.conf.urls.defaults import patterns, url, include +# Handle django.utils.encoding rename: +# smart_unicode -> smart_text +# force_unicode -> force_text +try: + from django.utils.encoding import smart_text +except ImportError: + from django.utils.encoding import smart_unicode as smart_text +try: + from django.utils.encoding import force_text +except ImportError: + from django.utils.encoding import force_unicode as force_text + + # django-filter is optional try: import django_filters @@ -25,9 +43,9 @@ except: try: import cStringIO.StringIO as StringIO except ImportError: - from six import StringIO + StringIO = six.StringIO -from six import BytesIO +BytesIO = six.BytesIO # urlparse compat import (Required because it changed in python 3.x) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index e59cc9b42..adea5bf5a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import six import copy import datetime @@ -14,14 +13,12 @@ from django.conf import settings from django import forms from django.forms import widgets from django.utils.encoding import is_protected_type -try: - from django.utils.encoding import smart_text -except ImportError: - from django.utils.encoding import smart_unicode as smart_text from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import parse_date, parse_datetime from rest_framework.compat import timezone from rest_framework.compat import BytesIO +from rest_framework.compat import six +from rest_framework.compat import smart_text def is_simple_callable(obj): diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index d5cfaaf88..7c01006ab 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -5,14 +5,13 @@ They give us a generic way of being able to handle various media types on the request, such as form content or json encoded data. """ -import six - from django.http import QueryDict from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError from django.utils import simplejson as json from rest_framework.compat import yaml, ETParseError from rest_framework.exceptions import ParseError +from rest_framework.compat import six from xml.etree import ElementTree as ET from xml.parsers.expat import ExpatError import datetime diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 33d3732f3..b7a6e0c10 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -6,13 +6,10 @@ from django.core.urlresolvers import resolve, get_script_prefix from django import forms from django.forms import widgets from django.forms.models import ModelChoiceIterator -try: - from django.utils.encoding import smart_text -except ImportError: - from django.utils.encoding import smart_unicode as smart_text from rest_framework.fields import Field, WritableField from rest_framework.reverse import reverse from rest_framework.compat import urlparse +from rest_framework.compat import smart_text ##### Relational fields ##### diff --git a/rest_framework/request.py b/rest_framework/request.py index c50ae5ad7..048a1c41d 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -9,7 +9,6 @@ The wrapped request then offers a richer API, in particular : - full support of PUT method, including support for file uploads - form overloading of HTTP method, content type and content """ -import six from rest_framework.compat import BytesIO from django.http.multipartparser import parse_header diff --git a/rest_framework/response.py b/rest_framework/response.py index cad95611c..0a484c4a1 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -1,8 +1,8 @@ -import six - from django.core.handlers.wsgi import STATUS_CODE_TEXT from django.template.response import SimpleTemplateResponse +from rest_framework.compat import six + class Response(SimpleTemplateResponse): """ diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9f35f77cf..663f166b9 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1,5 +1,3 @@ -import six - import copy import datetime import types @@ -8,6 +6,7 @@ from django.db import models from django.forms import widgets from django.utils.datastructures import SortedDict from rest_framework.compat import get_concrete_model +from rest_framework.compat import six # Note: We do the following so that users of the framework can use this style: # diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 9e73bbfb7..186833b5f 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -19,8 +19,7 @@ back to the defaults. """ from django.conf import settings from django.utils import importlib -from six import string_types - +from rest_framework.compat import six USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None) @@ -100,7 +99,7 @@ def perform_import(val, setting_name): If the given setting is a string import notation, then perform the necessary import or imports. """ - if isinstance(val, string_types): + if isinstance(val, six.string_types): return import_from_string(val, setting_name) elif isinstance(val, (list, tuple)): return [import_from_string(item, setting_name) for item in val] @@ -118,6 +117,7 @@ def import_from_string(val, setting_name): module = importlib.import_module(module_path) return getattr(module, class_name) except: + raise msg = "Could not import '%s' for API setting '%s'" % (val, setting_name) raise ImportError(msg) diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 52c7a59ce..4205e57cc 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -1,16 +1,14 @@ from __future__ import unicode_literals, absolute_import -import six from django import template from django.core.urlresolvers import reverse from django.http import QueryDict -try: - from django.utils.encoding import force_text -except ImportError: - from django.utils.encoding import force_unicode as force_text from django.utils.html import escape from django.utils.safestring import SafeData, mark_safe from rest_framework.compat import urlparse +from rest_framework.compat import force_text +from rest_framework.compat import six + import re import string diff --git a/rest_framework/tests/files.py b/rest_framework/tests/files.py index 42e8ed5fc..ca6bc9050 100644 --- a/rest_framework/tests/files.py +++ b/rest_framework/tests/files.py @@ -1,11 +1,10 @@ -from rest_framework.compat import BytesIO - import datetime -import six from django.test import TestCase from rest_framework import serializers +from rest_framework.compat import BytesIO +from rest_framework.compat import six class UploadedFile(object): diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index a877574ef..215de0c45 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -1,14 +1,12 @@ from __future__ import unicode_literals -import six - from django.db import models from django.test import TestCase from django.test.client import RequestFactory from django.utils import simplejson as json from rest_framework import generics, serializers, status from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel - +from rest_framework.compat import six factory = RequestFactory() diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py index d4662465e..34caa208b 100644 --- a/rest_framework/tests/htmlrenderer.py +++ b/rest_framework/tests/htmlrenderer.py @@ -1,5 +1,3 @@ -import six - from django.core.exceptions import PermissionDenied from django.http import Http404 from django.test import TestCase @@ -9,6 +7,7 @@ from rest_framework.compat import patterns, url from rest_framework.decorators import api_view, renderer_classes from rest_framework.renderers import TemplateHTMLRenderer from rest_framework.response import Response +from rest_framework.compat import six @api_view(('GET',)) diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index b02fccf48..724053360 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -1,6 +1,5 @@ import pickle import re -import six from django.core.cache import cache from django.test import TestCase @@ -16,6 +15,7 @@ from rest_framework.parsers import YAMLParser, XMLParser from rest_framework.settings import api_settings from rest_framework.compat import StringIO +from rest_framework.compat import six import datetime from decimal import Decimal diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index fe5116a81..7d4575bb9 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -1,8 +1,6 @@ """ Tests for content parsing, and form-overloaded content parsing. """ -import six - from django.contrib.auth.models import User from django.contrib.auth import authenticate, login, logout from django.contrib.sessions.middleware import SessionMiddleware @@ -22,6 +20,7 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import APIView +from rest_framework.compat import six factory = RequestFactory() diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py index fd153f400..453488d00 100644 --- a/rest_framework/tests/response.py +++ b/rest_framework/tests/response.py @@ -1,6 +1,3 @@ -import unittest -import six - from django.test import TestCase from rest_framework.compat import patterns, url, include from rest_framework.response import Response @@ -12,6 +9,7 @@ from rest_framework.renderers import ( BrowsableAPIRenderer ) from rest_framework.settings import api_settings +from rest_framework.compat import six class MockPickleRenderer(BaseRenderer): diff --git a/rest_framework/utils/__init__.py b/rest_framework/utils/__init__.py index 458793539..1603f9725 100644 --- a/rest_framework/utils/__init__.py +++ b/rest_framework/utils/__init__.py @@ -1,13 +1,7 @@ - -import six - -try: - from django.utils.encoding import smart_text -except ImportError: - from django.utils.encoding import smart_unicode as smart_text - from django.utils.xmlutils import SimplerXMLGenerator from rest_framework.compat import StringIO +from rest_framework.compat import six +from rest_framework.compat import smart_text import re import xml.etree.ElementTree as ET From acc97a8a077cee79da28cadc7dc1cda9e768fce4 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 3 Jan 2013 11:41:26 +0100 Subject: [PATCH 31/63] Don't require six --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dd017c77d..33d35d284 100755 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ setup( packages=get_packages('rest_framework'), package_data=get_package_data('rest_framework'), test_suite='rest_framework.runtests.runtests.main', - install_requires=['six'], + install_requires=[], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', From bbc11463203da3e76120596df661a8cc4ef3bf7e Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 3 Jan 2013 11:41:37 +0100 Subject: [PATCH 32/63] Python 3 readiness --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 33d35d284..640bac4d9 100755 --- a/setup.py +++ b/setup.py @@ -74,6 +74,7 @@ setup( 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 3', 'Topic :: Internet :: WWW/HTTP', ] ) From 06ae47752f8e6fb1605e887b613441f0f72918e6 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 3 Jan 2013 12:49:57 +0100 Subject: [PATCH 33/63] Also use the compat module in that file. --- rest_framework/authentication.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 15e531bf6..42f6f02b7 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -4,12 +4,9 @@ Provides a set of pluggable authentication policies. from django.contrib.auth import authenticate from django.utils.encoding import DjangoUnicodeDecodeError -try: - from django.utils.encoding import smart_text -except ImportError: - from django.utils.encoding import smart_unicode as smart_text from rest_framework import exceptions from rest_framework.compat import CsrfViewMiddleware +from rest_framework.compat import smart_text from rest_framework.authtoken.models import Token import base64 From fccf1814f1af153ede074dcdad12f02b407b092d Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 7 Jan 2013 22:23:16 +0100 Subject: [PATCH 34/63] Test against Django 1.5 rc1 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2eb2ada2a..1ce287c2f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: env: - DJANGO=https://github.com/django/django/zipball/master - - DJANGO=https://www.djangoproject.com/download/1.5b2/tarball/ + - DJANGO=https://www.djangoproject.com/download/1.5c1/tarball/ - DJANGO=django==1.4.3 --use-mirrors - DJANGO=django==1.3.5 --use-mirrors From c1e3b42fee73b47d5aa7330cdf8946657742d6f7 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 7 Jan 2013 22:41:42 +0100 Subject: [PATCH 35/63] Conditional six installation (only for django 1.3.5) and install 3.2 compatible django-filter for python 3.2 test. --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1ce287c2f..e87ca38bc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,8 @@ env: install: - pip install $DJANGO - pip install django-filter==0.5.4 --use-mirrors - - pip install six --use-mirrors + - "if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install --upgrade -e git+https://github.com/alex/django-filter.git#egg=django-filter; fi" + - "if [[ $DJANGO == 'django==1.3.5 --use-mirrors' ]] then pip install six --use-mirrors; fi" - export PYTHONPATH=. script: From 22a7dc27d8638b001d513598e5f0ba13698a1186 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 7 Jan 2013 23:09:47 +0100 Subject: [PATCH 36/63] Typo in the travis.yml. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e87ca38bc..2cad33ab2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ install: - pip install $DJANGO - pip install django-filter==0.5.4 --use-mirrors - "if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install --upgrade -e git+https://github.com/alex/django-filter.git#egg=django-filter; fi" - - "if [[ $DJANGO == 'django==1.3.5 --use-mirrors' ]] then pip install six --use-mirrors; fi" + - "if [[ $DJANGO == 'django==1.3.5 --use-mirrors' ]]; then pip install six --use-mirrors; fi" - export PYTHONPATH=. script: From 510d6a3c5540bbc20406ff79ce5f95d97b2a63f3 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 7 Jan 2013 23:26:14 +0100 Subject: [PATCH 37/63] Introduced HTTP_HEADER_ENCODING. --- rest_framework/authentication.py | 5 ++++- rest_framework/settings.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 42f6f02b7..c50bf9441 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -8,6 +8,7 @@ from rest_framework import exceptions from rest_framework.compat import CsrfViewMiddleware from rest_framework.compat import smart_text from rest_framework.authtoken.models import Token +from rest_framework.settings import api_settings import base64 @@ -37,7 +38,9 @@ class BasicAuthentication(BaseAuthentication): auth = request.META['HTTP_AUTHORIZATION'].split() if len(auth) == 2 and auth[0].lower() == "basic": try: - auth_parts = base64.b64decode(auth[1].encode('iso-8859-1')).decode('iso-8859-1').partition(':') + encoding = api_settings.HTTP_HEADER_ENCODING + b = base64.b64decode(auth[1].encode(encoding)) + auth_parts = b.decode(encoding).partition(':') except TypeError: return None diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 186833b5f..2358d188d 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -75,6 +75,9 @@ DEFAULTS = { 'URL_FORMAT_OVERRIDE': 'format', 'FORMAT_SUFFIX_KWARG': 'format', + + # Header encoding (see RFC5987) + 'HTTP_HEADER_ENCODING': 'iso-8859-1', } From dd456420551449e1e8f3cff798908f9f8256fcdc Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 7 Jan 2013 23:34:03 +0100 Subject: [PATCH 38/63] Try a lighter alternative to install django-filter trunk --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2cad33ab2..1b3304414 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ env: install: - pip install $DJANGO - pip install django-filter==0.5.4 --use-mirrors - - "if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install --upgrade -e git+https://github.com/alex/django-filter.git#egg=django-filter; fi" + - "if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install --upgrade https://github.com/alex/django-filter/tarball/master; fi" - "if [[ $DJANGO == 'django==1.3.5 --use-mirrors' ]]; then pip install six --use-mirrors; fi" - export PYTHONPATH=. From 86a3ad9b3d4f14e4bd6f834345030bde4ad34835 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 7 Jan 2013 23:36:39 +0100 Subject: [PATCH 39/63] Check for travis builds. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1b3304414..00b544e76 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ install: - pip install django-filter==0.5.4 --use-mirrors - "if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install --upgrade https://github.com/alex/django-filter/tarball/master; fi" - "if [[ $DJANGO == 'django==1.3.5 --use-mirrors' ]]; then pip install six --use-mirrors; fi" + - "echo $DJANGO" - export PYTHONPATH=. script: From 28248f35736a9eb987ca4d552417a0abc60c5a74 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 7 Jan 2013 23:46:44 +0100 Subject: [PATCH 40/63] Attempt with another url for django --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 00b544e76..e205a484e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,14 +7,14 @@ python: env: - DJANGO=https://github.com/django/django/zipball/master - - DJANGO=https://www.djangoproject.com/download/1.5c1/tarball/ + - DJANGO=http://optiminformatique.fr/media/Django-1.5c1.tar.gz - DJANGO=django==1.4.3 --use-mirrors - DJANGO=django==1.3.5 --use-mirrors install: - pip install $DJANGO - pip install django-filter==0.5.4 --use-mirrors - - "if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install --upgrade https://github.com/alex/django-filter/tarball/master; fi" + - "if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install https://github.com/alex/django-filter/tarball/master; fi" - "if [[ $DJANGO == 'django==1.3.5 --use-mirrors' ]]; then pip install six --use-mirrors; fi" - "echo $DJANGO" - export PYTHONPATH=. From 0252057ee004cd236a3986892309c8808bd79efe Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 7 Jan 2013 23:52:26 +0100 Subject: [PATCH 41/63] Reverted on the right django 1.5 rc1 url and made sure we use mirrors for our django downloads. --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index e205a484e..10a4a7713 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,9 +7,9 @@ python: env: - DJANGO=https://github.com/django/django/zipball/master - - DJANGO=http://optiminformatique.fr/media/Django-1.5c1.tar.gz - - DJANGO=django==1.4.3 --use-mirrors - - DJANGO=django==1.3.5 --use-mirrors + - DJANGO=https://www.djangoproject.com/download/1.5c1/tarball/ + - DJANGO="django==1.4.3 --use-mirrors" + - DJANGO="django==1.3.5 --use-mirrors" install: - pip install $DJANGO From 5dd76c07065cf0df16a7ae581856e1730d2696e8 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 7 Jan 2013 23:55:16 +0100 Subject: [PATCH 42/63] Correctly remove from the matrix incompatible django / python versions. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 10a4a7713..556a26422 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,6 @@ script: matrix: exclude: - python: "3.2" - env: DJANGO=django==1.4.3 --use-mirrors + env: DJANGO="django==1.4.3 --use-mirrors" - python: "3.2" - env: DJANGO=django==1.3.5 --use-mirrors + env: DJANGO="django==1.3.5 --use-mirrors" From 0f0e76d8b1b7a7a28b4ce2c6d8f7ecc89e7219ff Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 8 Jan 2013 00:00:28 +0100 Subject: [PATCH 43/63] Don't double install django-filter. --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 556a26422..572e483b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,10 +13,9 @@ env: install: - pip install $DJANGO - - pip install django-filter==0.5.4 --use-mirrors + - "if [[ $TRAVIS_PYTHON_VERSION != '3.2' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" - "if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install https://github.com/alex/django-filter/tarball/master; fi" - "if [[ $DJANGO == 'django==1.3.5 --use-mirrors' ]]; then pip install six --use-mirrors; fi" - - "echo $DJANGO" - export PYTHONPATH=. script: From 4eb5861f3676781493af29f8e9fd87ec22e591aa Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 18 Jan 2013 23:36:35 +0000 Subject: [PATCH 44/63] Starting migration from ManyField to Field(many=True) --- rest_framework/relations.py | 93 ++++++++++++++---------------- rest_framework/tests/serializer.py | 4 +- 2 files changed, 47 insertions(+), 50 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index af63ceaaa..8d3615adc 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -21,15 +21,20 @@ class RelatedField(WritableField): representation of the target. """ widget = widgets.Select + many_widget = widgets.SelectMultiple cache_choices = False empty_label = None default_read_only = True # TODO: Remove this + many = False def __init__(self, *args, **kwargs): self.queryset = kwargs.pop('queryset', None) self.null = kwargs.pop('null', False) + self.many = kwargs.pop('many', self.many) super(RelatedField, self).__init__(*args, **kwargs) self.read_only = kwargs.pop('read_only', self.default_read_only) + if self.many: + self.widget = self.many_widget def initialize(self, parent, field_name): super(RelatedField, self).initialize(parent, field_name) @@ -108,6 +113,9 @@ class RelatedField(WritableField): if value is None: return None + + if self.many: + return [self.to_native(item) for item in value.all()] return self.to_native(value) def field_from_native(self, data, files, field_name, into): @@ -115,7 +123,17 @@ class RelatedField(WritableField): return try: - value = data[field_name] + if self.many: + try: + # Form data + value = data.getlist(field_name) + if value == ['']: + value = [] + except AttributeError: + # Non-form data + value = data[field_name] + else: + value = data[field_name] except KeyError: if self.required: raise ValidationError(self.error_messages['required']) @@ -125,47 +143,12 @@ class RelatedField(WritableField): raise ValidationError('Value may not be null') elif value in (None, '') and self.null: into[(self.source or field_name)] = None + elif self.many: + into[(self.source or field_name)] = [self.from_native(item) for item in value] else: into[(self.source or field_name)] = self.from_native(value) -class ManyRelatedMixin(object): - """ - Mixin to convert a related field to a many related field. - """ - widget = widgets.SelectMultiple - - def field_to_native(self, obj, field_name): - value = getattr(obj, self.source or field_name) - return [self.to_native(item) for item in value.all()] - - def field_from_native(self, data, files, field_name, into): - if self.read_only: - return - - try: - # Form data - value = data.getlist(self.source or field_name) - except: - # Non-form data - value = data.get(self.source or field_name) - else: - if value == ['']: - value = [] - - into[field_name] = [self.from_native(item) for item in value] - - -class ManyRelatedField(ManyRelatedMixin, RelatedField): - """ - Base class for related model managers. - - If not overridden, this represents a to-many relationship, using the unicode - representations of the target, and is read-only. - """ - pass - - ### PrimaryKey relationships class PrimaryKeyRelatedField(RelatedField): @@ -227,6 +210,12 @@ class PrimaryKeyRelatedField(RelatedField): return self.to_native(pk) +class ManyRelatedField(RelatedField): + def __init__(self, *args, **kwargs): + kwargs['many'] = True + super(ManyRelatedField, self).__init__(*args, **kwargs) + + class ManyPrimaryKeyRelatedField(ManyRelatedField): """ Represents a to-many relationship as a pk value. @@ -314,10 +303,6 @@ class SlugRelatedField(RelatedField): raise ValidationError(msg) -class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField): - form_field_class = forms.MultipleChoiceField - - ### Hyperlinked relationships class HyperlinkedRelatedField(RelatedField): @@ -442,13 +427,6 @@ class HyperlinkedRelatedField(RelatedField): return obj -class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField): - """ - Represents a to-many relationship, using hyperlinking. - """ - form_field_class = forms.MultipleChoiceField - - class HyperlinkedIdentityField(Field): """ Represents the instance, or a property on the instance, using hyperlinking. @@ -512,3 +490,20 @@ class HyperlinkedIdentityField(Field): pass raise Exception('Could not resolve URL for field using view name "%s"' % view_name) + + +### Old-style many classes for backwards compat + + + + +class ManySlugRelatedField(SlugRelatedField): + def __init__(self, *args, **kwargs): + kwargs['many'] = True + super(ManySlugRelatedField, self).__init__(*args, **kwargs) + + +class ManyHyperlinkedRelatedField(HyperlinkedRelatedField): + def __init__(self, *args, **kwargs): + kwargs['many'] = True + super(ManyHyperlinkedRelatedField, self).__init__(*args, **kwargs) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index bd96ba23e..b2d62adef 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1,5 +1,6 @@ import datetime import pickle +from django.utils.datastructures import MultiValueDict from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, @@ -479,7 +480,8 @@ class ManyToManyTests(TestCase): containing no items, using a representation that does not support lists (eg form data). """ - data = {'rel': ''} + data = MultiValueDict() + data.setlist('rel', ['']) serializer = self.serializer_class(data=data) self.assertEquals(serializer.is_valid(), True) instance = serializer.save() From 4601487248ef76537e5e3fda17ae7220191dbad9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 30 Jan 2013 12:41:18 +0000 Subject: [PATCH 45/63] Use many=True style for pk relations. --- rest_framework/relations.py | 91 +++++++++++-------------------------- rest_framework/renderers.py | 6 ++- 2 files changed, 32 insertions(+), 65 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 8d3615adc..aee43206e 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -22,6 +22,9 @@ class RelatedField(WritableField): """ widget = widgets.Select many_widget = widgets.SelectMultiple + form_field_class = forms.ChoiceField + many_form_field_class = forms.MultipleChoiceField + cache_choices = False empty_label = None default_read_only = True # TODO: Remove this @@ -156,7 +159,6 @@ class PrimaryKeyRelatedField(RelatedField): Represents a to-one relationship as a pk value. """ default_read_only = False - form_field_class = forms.ChoiceField default_error_messages = { 'does_not_exist': _("Invalid pk '%s' - object does not exist."), @@ -196,85 +198,38 @@ class PrimaryKeyRelatedField(RelatedField): raise ValidationError(msg) def field_to_native(self, obj, field_name): + if self.many: + # To-many relationship + try: + # Prefer obj.serializable_value for performance reasons + queryset = obj.serializable_value(self.source or field_name) + except AttributeError: + # RelatedManager (reverse relationship) + queryset = getattr(obj, self.source or field_name) + + # Forward relationship + return [self.to_native(item.pk) for item in queryset.all()] + + # To-one relationship try: # Prefer obj.serializable_value for performance reasons pk = obj.serializable_value(self.source or field_name) except AttributeError: # RelatedObject (reverse relationship) try: - obj = getattr(obj, self.source or field_name) + pk = getattr(obj, self.source or field_name).pk except ObjectDoesNotExist: return None - return self.to_native(obj.pk) + # Forward relationship return self.to_native(pk) -class ManyRelatedField(RelatedField): - def __init__(self, *args, **kwargs): - kwargs['many'] = True - super(ManyRelatedField, self).__init__(*args, **kwargs) - - -class ManyPrimaryKeyRelatedField(ManyRelatedField): - """ - Represents a to-many relationship as a pk value. - """ - default_read_only = False - form_field_class = forms.MultipleChoiceField - - default_error_messages = { - 'does_not_exist': _("Invalid pk '%s' - object does not exist."), - 'incorrect_type': _('Incorrect type. Expected pk value, received %s.'), - } - - def prepare_value(self, obj): - return self.to_native(obj.pk) - - def label_from_instance(self, obj): - """ - Return a readable representation for use with eg. select widgets. - """ - desc = smart_unicode(obj) - ident = smart_unicode(self.to_native(obj.pk)) - if desc == ident: - return desc - return "%s - %s" % (desc, ident) - - def to_native(self, pk): - return pk - - def field_to_native(self, obj, field_name): - try: - # Prefer obj.serializable_value for performance reasons - queryset = obj.serializable_value(self.source or field_name) - except AttributeError: - # RelatedManager (reverse relationship) - queryset = getattr(obj, self.source or field_name) - return [self.to_native(item.pk) for item in queryset.all()] - # Forward relationship - return [self.to_native(item.pk) for item in queryset.all()] - - def from_native(self, data): - if self.queryset is None: - raise Exception('Writable related fields must include a `queryset` argument') - - try: - return self.queryset.get(pk=data) - except ObjectDoesNotExist: - msg = self.error_messages['does_not_exist'] % smart_unicode(data) - raise ValidationError(msg) - except (TypeError, ValueError): - received = type(data).__name__ - msg = self.error_messages['incorrect_type'] % received - raise ValidationError(msg) - ### Slug relationships class SlugRelatedField(RelatedField): default_read_only = False - form_field_class = forms.ChoiceField default_error_messages = { 'does_not_exist': _("Object with %s=%s does not exist."), @@ -313,7 +268,6 @@ class HyperlinkedRelatedField(RelatedField): slug_field = 'slug' slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden default_read_only = False - form_field_class = forms.ChoiceField default_error_messages = { 'no_match': _('Invalid hyperlink - No URL match'), @@ -494,8 +448,17 @@ class HyperlinkedIdentityField(Field): ### Old-style many classes for backwards compat +class ManyRelatedField(RelatedField): + def __init__(self, *args, **kwargs): + kwargs['many'] = True + super(ManyRelatedField, self).__init__(*args, **kwargs) +class ManyPrimaryKeyRelatedField(PrimaryKeyRelatedField): + def __init__(self, *args, **kwargs): + kwargs['many'] = True + super(ManyPrimaryKeyRelatedField, self).__init__(*args, **kwargs) + class ManySlugRelatedField(SlugRelatedField): def __init__(self, *args, **kwargs): diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 0a34abaa0..1f6e615f0 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -332,7 +332,11 @@ class BrowsableAPIRenderer(BaseRenderer): kwargs['label'] = k - fields[k] = v.form_field_class(**kwargs) + if getattr(v, 'many', None): + fields[k] = v.many_form_field_class(**kwargs) + else: + fields[k] = v.form_field_class(**kwargs) + return fields def get_form(self, view, method, request): From 9a4d01d687d57601d37f9a930d37039cb9f6a6f2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 30 Jan 2013 12:41:26 +0000 Subject: [PATCH 46/63] Formatting fixes --- rest_framework/fields.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 998911e12..a788ecf2f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -325,7 +325,8 @@ class ChoiceField(WritableField): form_field_class = forms.ChoiceField widget = widgets.Select default_error_messages = { - 'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'), + 'invalid_choice': _('Select a valid choice. %(value)s is not one of ' + 'the available choices.'), } def __init__(self, choices=(), *args, **kwargs): @@ -612,7 +613,8 @@ class ImageField(FileField): form_field_class = forms.ImageField default_error_messages = { - 'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."), + 'invalid_image': _("Upload a valid image. The file you uploaded was " + "either not an image or a corrupted image."), } def from_native(self, data): From e24d29ec05fd3fa7cc12533929dc1b68e9d56f9a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 30 Jan 2013 14:21:18 +0000 Subject: [PATCH 47/63] Tweak empty list implementation --- rest_framework/relations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 221c72fba..046b0db17 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -129,8 +129,8 @@ class RelatedField(WritableField): try: # Form data value = data.getlist(field_name) - if value == ['']: - value = [] + if value == [''] or value == []: + raise KeyError except AttributeError: # Non-form data value = data[field_name] From e4ac566625dcb2858ce15148e38595b49eedfa1b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 30 Jan 2013 20:33:50 +0000 Subject: [PATCH 48/63] Add dprecation warnings --- rest_framework/fields.py | 26 +++++++++++--------------- rest_framework/relations.py | 32 +++++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d6689c4e7..b9b7e4260 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -33,7 +33,7 @@ class Field(object): empty = '' type_name = None partial = False - _use_files = None + use_files = False form_field_class = forms.CharField def __init__(self, source=None): @@ -126,6 +126,13 @@ class WritableField(Field): validators=[], error_messages=None, widget=None, default=None, blank=None): + # 'blank' is to be deprecated in favor of 'required' + if blank is not None: + warnings.warn('The `blank` keyword argument is due to deprecated. ' + 'Use the `required` keyword argument instead.', + PendingDeprecationWarning, stacklevel=2) + required = not(blank) + super(WritableField, self).__init__(source=source) self.read_only = read_only @@ -143,7 +150,6 @@ class WritableField(Field): self.validators = self.default_validators + validators self.default = default if default is not None else self.default - self.blank = blank # Widgets are ony used for HTML forms. widget = widget or self.widget @@ -182,7 +188,7 @@ class WritableField(Field): return try: - if self._use_files: + if self.use_files: files = files or {} native = files[field_name] else: @@ -289,16 +295,6 @@ class CharField(WritableField): if max_length is not None: self.validators.append(validators.MaxLengthValidator(max_length)) - def validate(self, value): - """ - Validates that the value is supplied (if required). - """ - # if empty string and allow blank - if self.blank and not value: - return - else: - super(CharField, self).validate(value) - def from_native(self, value): if isinstance(value, basestring) or value is None: return value @@ -567,7 +563,7 @@ class FloatField(WritableField): class FileField(WritableField): - _use_files = True + use_files = True type_name = 'FileField' form_field_class = forms.FileField widget = widgets.FileInput @@ -611,7 +607,7 @@ class FileField(WritableField): class ImageField(FileField): - _use_files = True + use_files = True form_field_class = forms.ImageField default_error_messages = { diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 046b0db17..d49ca39b2 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework.fields import Field, WritableField from rest_framework.reverse import reverse from urlparse import urlparse +import warnings ##### Relational fields ##### @@ -26,23 +27,27 @@ class RelatedField(WritableField): cache_choices = False empty_label = None - default_read_only = True # TODO: Remove this + read_only = True many = False def __init__(self, *args, **kwargs): - # 'null' will be deprecated in favor of 'required' + # 'null' is to be deprecated in favor of 'required' if 'null' in kwargs: + warnings.warn('The `null` keyword argument is due to be deprecated. ' + 'Use the `required` keyword argument instead.', + PendingDeprecationWarning, stacklevel=2) kwargs['required'] = not kwargs.pop('null') self.queryset = kwargs.pop('queryset', None) self.many = kwargs.pop('many', self.many) - super(RelatedField, self).__init__(*args, **kwargs) - self.read_only = kwargs.pop('read_only', self.default_read_only) if self.many: self.widget = self.many_widget self.form_field_class = self.many_form_field_class + kwargs['read_only'] = kwargs.pop('read_only', self.read_only) + super(RelatedField, self).__init__(*args, **kwargs) + def initialize(self, parent, field_name): super(RelatedField, self).initialize(parent, field_name) if self.queryset is None and not self.read_only: @@ -157,7 +162,7 @@ class PrimaryKeyRelatedField(RelatedField): """ Represents a relationship as a pk value. """ - default_read_only = False + read_only = False default_error_messages = { 'does_not_exist': _("Invalid pk '%s' - object does not exist."), @@ -231,7 +236,7 @@ class SlugRelatedField(RelatedField): """ Represents a relationship using a unique field on the target. """ - default_read_only = False + read_only = False default_error_messages = { 'does_not_exist': _("Object with %s=%s does not exist."), @@ -269,7 +274,7 @@ class HyperlinkedRelatedField(RelatedField): pk_url_kwarg = 'pk' slug_field = 'slug' slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden - default_read_only = False + read_only = False default_error_messages = { 'no_match': _('Invalid hyperlink - No URL match'), @@ -390,6 +395,7 @@ class HyperlinkedIdentityField(Field): pk_url_kwarg = 'pk' slug_field = 'slug' slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden + read_only = True def __init__(self, *args, **kwargs): # TODO: Make view_name mandatory, and have the @@ -452,23 +458,35 @@ class HyperlinkedIdentityField(Field): class ManyRelatedField(RelatedField): def __init__(self, *args, **kwargs): + warnings.warn('`ManyRelatedField()` is due to be deprecated. ' + 'Use `RelatedField(many=True)` instead.', + PendingDeprecationWarning, stacklevel=2) kwargs['many'] = True super(ManyRelatedField, self).__init__(*args, **kwargs) class ManyPrimaryKeyRelatedField(PrimaryKeyRelatedField): def __init__(self, *args, **kwargs): + warnings.warn('`ManyPrimaryKeyRelatedField()` is due to be deprecated. ' + 'Use `PrimaryKeyRelatedField(many=True)` instead.', + PendingDeprecationWarning, stacklevel=2) kwargs['many'] = True super(ManyPrimaryKeyRelatedField, self).__init__(*args, **kwargs) class ManySlugRelatedField(SlugRelatedField): def __init__(self, *args, **kwargs): + warnings.warn('`ManySlugRelatedField()` is due to be deprecated. ' + 'Use `SlugRelatedField(many=True)` instead.', + PendingDeprecationWarning, stacklevel=2) kwargs['many'] = True super(ManySlugRelatedField, self).__init__(*args, **kwargs) class ManyHyperlinkedRelatedField(HyperlinkedRelatedField): def __init__(self, *args, **kwargs): + warnings.warn('`ManyHyperlinkedRelatedField()` is due to be deprecated. ' + 'Use `HyperlinkedRelatedField(many=True)` instead.', + PendingDeprecationWarning, stacklevel=2) kwargs['many'] = True super(ManyHyperlinkedRelatedField, self).__init__(*args, **kwargs) From f1e665673feeca945561a7b7cef251c86254eed8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 30 Jan 2013 20:38:11 +0000 Subject: [PATCH 49/63] Tidying --- rest_framework/serializers.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d02e1ada9..258f9d629 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -95,15 +95,19 @@ class SerializerOptions(object): class BaseSerializer(Field): + """ + This is the Serializer implementation. + We need to implement it as `BaseSerializer` due to metaclass magicks. + """ class Meta(object): pass _options_class = SerializerOptions - _dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations. + _dict_class = SortedDictWithMetadata def __init__(self, instance=None, data=None, files=None, - context=None, partial=False, **kwargs): - super(BaseSerializer, self).__init__(**kwargs) + context=None, partial=False, source=None): + super(BaseSerializer, self).__init__(source=source) self.opts = self._options_class(self.Meta) self.parent = None self.root = None @@ -347,6 +351,9 @@ class BaseSerializer(Field): @property def data(self): + """ + Returns the serialized data on the serializer. + """ if self._data is None: self._data = self.to_native(self.object) return self._data From d9b73e15c87c3a7f11d6bea5ffd6118f86e40051 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 31 Jan 2013 17:06:23 +0000 Subject: [PATCH 50/63] Serializers take `many=` argument. --- rest_framework/serializers.py | 77 +++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 258f9d629..6b2a8368c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -106,12 +106,13 @@ class BaseSerializer(Field): _dict_class = SortedDictWithMetadata def __init__(self, instance=None, data=None, files=None, - context=None, partial=False, source=None): + context=None, partial=False, many=None, source=None): super(BaseSerializer, self).__init__(source=source) self.opts = self._options_class(self.Meta) self.parent = None self.root = None self.partial = partial + self.many = many self.context = context or {} @@ -191,22 +192,6 @@ class BaseSerializer(Field): """ return field_name - def convert_object(self, obj): - """ - Core of serialization. - Convert an object into a dictionary of serialized field values. - """ - ret = self._dict_class() - ret.fields = {} - - for field_name, field in self.fields.items(): - field.initialize(parent=self, field_name=field_name) - key = self.get_field_key(field_name) - value = field.field_to_native(obj, field_name) - ret[key] = value - ret.fields[key] = field - return ret - def restore_fields(self, data, files): """ Core of deserialization, together with `restore_object`. @@ -278,22 +263,21 @@ class BaseSerializer(Field): """ Serialize objects -> primitives. """ - # Note: At the moment we have an ugly hack to determine if we should - # walk over iterables. At some point, serializers will require an - # explicit `many=True` in order to iterate over a set, and this hack - # will disappear. - if hasattr(obj, '__iter__') and not isinstance(obj, Page): - return [self.convert_object(item) for item in obj] - return self.convert_object(obj) + ret = self._dict_class() + ret.fields = {} + + for field_name, field in self.fields.items(): + field.initialize(parent=self, field_name=field_name) + key = self.get_field_key(field_name) + value = field.field_to_native(obj, field_name) + ret[key] = value + ret.fields[key] = field + return ret def from_native(self, data, files): """ Deserialize primitives -> objects. """ - if hasattr(data, '__iter__') and not isinstance(data, dict): - # TODO: error data when deserializing lists - return [self.from_native(item, None) for item in data] - self._errors = {} if data is not None or files is not None: attrs = self.restore_fields(data, files) @@ -332,6 +316,13 @@ class BaseSerializer(Field): if obj is None: return None + if self.many is not None: + many = self.many + else: + many = hasattr(obj, '__iter__') and not isinstance(obj, Page) + + if many: + return [self.to_native(item) for item in obj] return self.to_native(obj) @property @@ -341,9 +332,20 @@ class BaseSerializer(Field): setting self.object if no errors occurred. """ if self._errors is None: - obj = self.from_native(self.init_data, self.init_files) + data, files = self.init_data, self.init_files + + if self.many is not None: + many = self.many + else: + many = hasattr(data, '__iter__') and not isinstance(data, dict) + + # TODO: error data when deserializing lists + if many: + ret = [self.from_native(item, None) for item in data] + ret = self.from_native(data, files) + if not self._errors: - self.object = obj + self.object = ret return self._errors def is_valid(self): @@ -355,7 +357,18 @@ class BaseSerializer(Field): Returns the serialized data on the serializer. """ if self._data is None: - self._data = self.to_native(self.object) + obj = self.object + + if self.many is not None: + many = self.many + else: + many = hasattr(obj, '__iter__') and not isinstance(obj, Page) + + if many: + self._data = [self.to_native(item) for item in obj] + else: + self._data = self.to_native(obj) + return self._data def save(self): @@ -607,6 +620,8 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions): class HyperlinkedModelSerializer(ModelSerializer): """ + A subclass of ModelSerializer that uses hyperlinked relationships, + instead of primary key relationships. """ _options_class = HyperlinkedModelSerializerOptions _default_view_name = '%(model_name)s-detail' From f4f237e3ee02fef4fd5f389bf4fb3bbdd00173bd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 1 Feb 2013 14:03:28 +0000 Subject: [PATCH 51/63] 3.2, 3.3 compat --- rest_framework/__init__.py | 3 + rest_framework/authentication.py | 19 ++-- rest_framework/relations.py | 2 +- rest_framework/serializers.py | 6 +- rest_framework/settings.py | 3 - rest_framework/tests/authentication.py | 11 +- rest_framework/tests/genericrelations.py | 12 +-- rest_framework/tests/relations_hyperlink.py | 8 +- rest_framework/tests/relations_nested.py | 4 +- rest_framework/tests/relations_pk.py | 8 +- rest_framework/tests/relations_slug.py | 108 ++++++++++---------- rest_framework/tests/serializer.py | 14 +-- rest_framework/tests/utils.py | 4 +- rest_framework/tests/validators.py | 2 +- tox.ini | 36 ++++--- 15 files changed, 131 insertions(+), 109 deletions(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index f9882c57e..80e2c4107 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,6 @@ __version__ = '2.1.17' VERSION = __version__ # synonym + +# Header encoding (see RFC5987) +HTTP_HEADER_ENCODING = 'iso-8859-1' diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 76ee4bd68..c15568db4 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -1,10 +1,11 @@ """ Provides a set of pluggable authentication policies. """ +from __future__ import unicode_literals from django.contrib.auth import authenticate from django.utils.encoding import DjangoUnicodeDecodeError -from rest_framework import exceptions +from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework.compat import CsrfViewMiddleware from rest_framework.compat import smart_text from rest_framework.authtoken.models import Token @@ -43,23 +44,25 @@ class BasicAuthentication(BaseAuthentication): Returns a `User` if a correct username and password have been supplied using HTTP Basic authentication. Otherwise returns `None`. """ - auth = request.META.get('HTTP_AUTHORIZATION', '').split() + auth = request.META.get('HTTP_AUTHORIZATION', b'') + if type(auth) == type(''): + # Work around django test client oddness + auth = auth.encode(HTTP_HEADER_ENCODING) + auth = auth.split() - if not auth or auth[0].lower() != "basic": + if not auth or auth[0].lower() != b'basic': return None if len(auth) != 2: raise exceptions.AuthenticationFailed('Invalid basic header') - encoding = api_settings.HTTP_HEADER_ENCODING try: - auth_parts = base64.b64decode(auth[1].encode(encoding)).partition(':') - except TypeError: + auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':') + except (TypeError, UnicodeDecodeError): raise exceptions.AuthenticationFailed('Invalid basic header') try: - userid = smart_text(auth_parts[0]) - password = smart_text(auth_parts[2]) + userid, password = auth_parts[0], auth_parts[2] except DjangoUnicodeDecodeError: raise exceptions.AuthenticationFailed('Invalid basic header') diff --git a/rest_framework/relations.py b/rest_framework/relations.py index c4f854eff..dfa80fb7b 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -311,7 +311,7 @@ class SlugRelatedField(RelatedField): return self.queryset.get(**{self.slug_field: data}) except ObjectDoesNotExist: raise ValidationError(self.error_messages['does_not_exist'] % - (self.slug_field, unicode(data))) + (self.slug_field, smart_text(data))) except (TypeError, ValueError): msg = self.error_messages['invalid'] raise ValidationError(msg) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 3d3bcb3c4..b154fcadd 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -212,7 +212,7 @@ class BaseSerializer(Field): reverted_data = {} if data is not None and not isinstance(data, dict): - self._errors['non_field_errors'] = [u'Invalid data'] + self._errors['non_field_errors'] = ['Invalid data'] return None for field_name, field in self.fields.items(): @@ -287,7 +287,7 @@ class BaseSerializer(Field): """ Deserialize primitives -> objects. """ - if hasattr(data, '__iter__') and not isinstance(data, dict): + if hasattr(data, '__iter__') and not isinstance(data, (dict, six.text_type)): # TODO: error data when deserializing lists return [self.from_native(item, None) for item in data] @@ -525,7 +525,7 @@ class ModelSerializer(Serializer): """ try: instance.full_clean(exclude=self.get_validation_exclusions()) - except ValidationError, err: + except ValidationError as err: self._errors = err.message_dict return None return instance diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 13d03e62f..b3ca01347 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -75,9 +75,6 @@ DEFAULTS = { 'URL_FORMAT_OVERRIDE': 'format', 'FORMAT_SUFFIX_KWARG': 'format', - - # Header encoding (see RFC5987) - 'HTTP_HEADER_ENCODING': 'iso-8859-1', } diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index ba2042cbb..7dde6d22a 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -1,6 +1,9 @@ +from __future__ import unicode_literals + from django.contrib.auth.models import User from django.http import HttpResponse from django.test import Client, TestCase +from rest_framework import HTTP_HEADER_ENCODING from rest_framework import permissions from rest_framework.authtoken.models import Token from rest_framework.authentication import TokenAuthentication, BasicAuthentication, SessionAuthentication @@ -41,13 +44,17 @@ class BasicAuthTests(TestCase): def test_post_form_passing_basic_auth(self): """Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF""" - auth = 'Basic %s' % base64.encodestring('%s:%s' % (self.username, self.password)).encode('iso-8859-1').strip().decode('iso-8859-1') + credentials = ('%s:%s' % (self.username, self.password)) + base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING) + auth = 'Basic %s' % base64_credentials response = self.csrf_client.post('/basic/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) def test_post_json_passing_basic_auth(self): """Ensure POSTing form over basic auth with correct credentials passes and does not require CSRF""" - auth = 'Basic %s' % base64.encodestring('%s:%s' % (self.username, self.password)).encode('iso-8859-1').strip().decode('iso-8859-1') + credentials = ('%s:%s' % (self.username, self.password)) + base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING) + auth = 'Basic %s' % base64_credentials response = self.csrf_client.post('/basic/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) diff --git a/rest_framework/tests/genericrelations.py b/rest_framework/tests/genericrelations.py index 72070a1ad..91a986043 100644 --- a/rest_framework/tests/genericrelations.py +++ b/rest_framework/tests/genericrelations.py @@ -86,16 +86,16 @@ class TestGenericRelations(TestCase): serializer = TagSerializer(Tag.objects.all()) expected = [ { - 'tag': u'django', - 'tagged_item': u'Bookmark: https://www.djangoproject.com/' + 'tag': 'django', + 'tagged_item': 'Bookmark: https://www.djangoproject.com/' }, { - 'tag': u'python', - 'tagged_item': u'Bookmark: https://www.djangoproject.com/' + 'tag': 'python', + 'tagged_item': 'Bookmark: https://www.djangoproject.com/' }, { - 'tag': u'reminder', - 'tagged_item': u'Note: Remember the milk' + 'tag': 'reminder', + 'tagged_item': 'Note: Remember the milk' } ] self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index b4ad31661..f2957abff 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -218,11 +218,11 @@ class HyperlinkedForeignKeyTests(TestCase): self.assertEquals(serializer.data, expected) def test_foreign_key_update_incorrect_type(self): - data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': 2} + data = {'url': '/foreignkeysource/1/', 'name': 'source-1', 'target': 2} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'target': [u'Incorrect type. Expected url string, received int.']}) + self.assertEquals(serializer.errors, {'target': ['Incorrect type. Expected url string, received int.']}) def test_reverse_foreign_key_update(self): data = {'url': '/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']} @@ -439,7 +439,7 @@ class HyperlinkedNullableOneToOneTests(TestCase): queryset = OneToOneTarget.objects.all() serializer = NullableOneToOneTargetSerializer(queryset) expected = [ - {'url': '/onetoonetarget/1/', 'name': u'target-1', 'nullable_source': '/nullableonetoonesource/1/'}, - {'url': '/onetoonetarget/2/', 'name': u'target-2', 'nullable_source': None}, + {'url': '/onetoonetarget/1/', 'name': 'target-1', 'nullable_source': '/nullableonetoonesource/1/'}, + {'url': '/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None}, ] self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py index e81f0e423..e9051e71f 100644 --- a/rest_framework/tests/relations_nested.py +++ b/rest_framework/tests/relations_nested.py @@ -109,7 +109,7 @@ class NestedNullableOneToOneTests(TestCase): queryset = OneToOneTarget.objects.all() serializer = NullableOneToOneTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'nullable_source': {'id': 1, 'name': u'source-1', 'target': 1}}, - {'id': 2, 'name': u'target-2', 'nullable_source': None}, + {'id': 1, 'name': 'target-1', 'nullable_source': {'id': 1, 'name': 'source-1', 'target': 1}}, + {'id': 2, 'name': 'target-2', 'nullable_source': None}, ] self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index 4d00795ae..ca7ac17e7 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -198,11 +198,11 @@ class PKForeignKeyTests(TestCase): self.assertEquals(serializer.data, expected) def test_foreign_key_update_incorrect_type(self): - data = {'id': 1, 'name': u'source-1', 'target': 'foo'} + data = {'id': 1, 'name': 'source-1', 'target': 'foo'} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'target': [u'Incorrect type. Expected pk value, received str.']}) + self.assertEquals(serializer.errors, {'target': ['Incorrect type. Expected pk value, received str.']}) def test_reverse_foreign_key_update(self): data = {'id': 2, 'name': 'target-2', 'sources': [1, 3]} @@ -415,7 +415,7 @@ class PKNullableOneToOneTests(TestCase): queryset = OneToOneTarget.objects.all() serializer = NullableOneToOneTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'nullable_source': 1}, - {'id': 2, 'name': u'target-2', 'nullable_source': None}, + {'id': 1, 'name': 'target-1', 'nullable_source': 1}, + {'id': 2, 'name': 'target-2', 'nullable_source': None}, ] self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/relations_slug.py b/rest_framework/tests/relations_slug.py index 37ccc75e7..b4c2cb5f3 100644 --- a/rest_framework/tests/relations_slug.py +++ b/rest_framework/tests/relations_slug.py @@ -39,9 +39,9 @@ class PKForeignKeyTests(TestCase): queryset = ForeignKeySource.objects.all() serializer = ForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': 'target-1'}, - {'id': 2, 'name': u'source-2', 'target': 'target-1'}, - {'id': 3, 'name': u'source-3', 'target': 'target-1'} + {'id': 1, 'name': 'source-1', 'target': 'target-1'}, + {'id': 2, 'name': 'source-2', 'target': 'target-1'}, + {'id': 3, 'name': 'source-3', 'target': 'target-1'} ] self.assertEquals(serializer.data, expected) @@ -49,13 +49,13 @@ class PKForeignKeyTests(TestCase): queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': ['source-1', 'source-2', 'source-3']}, - {'id': 2, 'name': u'target-2', 'sources': []}, + {'id': 1, 'name': 'target-1', 'sources': ['source-1', 'source-2', 'source-3']}, + {'id': 2, 'name': 'target-2', 'sources': []}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_update(self): - data = {'id': 1, 'name': u'source-1', 'target': 'target-2'} + data = {'id': 1, 'name': 'source-1', 'target': 'target-2'} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -66,21 +66,21 @@ class PKForeignKeyTests(TestCase): queryset = ForeignKeySource.objects.all() serializer = ForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': 'target-2'}, - {'id': 2, 'name': u'source-2', 'target': 'target-1'}, - {'id': 3, 'name': u'source-3', 'target': 'target-1'} + {'id': 1, 'name': 'source-1', 'target': 'target-2'}, + {'id': 2, 'name': 'source-2', 'target': 'target-1'}, + {'id': 3, 'name': 'source-3', 'target': 'target-1'} ] self.assertEquals(serializer.data, expected) def test_foreign_key_update_incorrect_type(self): - data = {'id': 1, 'name': u'source-1', 'target': 123} + data = {'id': 1, 'name': 'source-1', 'target': 123} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'target': [u'Object with name=123 does not exist.']}) + self.assertEquals(serializer.errors, {'target': ['Object with name=123 does not exist.']}) def test_reverse_foreign_key_update(self): - data = {'id': 2, 'name': u'target-2', 'sources': ['source-1', 'source-3']} + data = {'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']} instance = ForeignKeyTarget.objects.get(pk=2) serializer = ForeignKeyTargetSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -89,8 +89,8 @@ class PKForeignKeyTests(TestCase): queryset = ForeignKeyTarget.objects.all() new_serializer = ForeignKeyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': ['source-1', 'source-2', 'source-3']}, - {'id': 2, 'name': u'target-2', 'sources': []}, + {'id': 1, 'name': 'target-1', 'sources': ['source-1', 'source-2', 'source-3']}, + {'id': 2, 'name': 'target-2', 'sources': []}, ] self.assertEquals(new_serializer.data, expected) @@ -101,55 +101,55 @@ class PKForeignKeyTests(TestCase): queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': ['source-2']}, - {'id': 2, 'name': u'target-2', 'sources': ['source-1', 'source-3']}, + {'id': 1, 'name': 'target-1', 'sources': ['source-2']}, + {'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_create(self): - data = {'id': 4, 'name': u'source-4', 'target': 'target-2'} + data = {'id': 4, 'name': 'source-4', 'target': 'target-2'} serializer = ForeignKeySourceSerializer(data=data) serializer.is_valid() self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'source-4') + self.assertEqual(obj.name, 'source-4') # Ensure source 4 is added, and everything else is as expected queryset = ForeignKeySource.objects.all() serializer = ForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': 'target-1'}, - {'id': 2, 'name': u'source-2', 'target': 'target-1'}, - {'id': 3, 'name': u'source-3', 'target': 'target-1'}, - {'id': 4, 'name': u'source-4', 'target': 'target-2'}, + {'id': 1, 'name': 'source-1', 'target': 'target-1'}, + {'id': 2, 'name': 'source-2', 'target': 'target-1'}, + {'id': 3, 'name': 'source-3', 'target': 'target-1'}, + {'id': 4, 'name': 'source-4', 'target': 'target-2'}, ] self.assertEquals(serializer.data, expected) def test_reverse_foreign_key_create(self): - data = {'id': 3, 'name': u'target-3', 'sources': ['source-1', 'source-3']} + data = {'id': 3, 'name': 'target-3', 'sources': ['source-1', 'source-3']} serializer = ForeignKeyTargetSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'target-3') + self.assertEqual(obj.name, 'target-3') # Ensure target 3 is added, and everything else is as expected queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'sources': ['source-2']}, - {'id': 2, 'name': u'target-2', 'sources': []}, - {'id': 3, 'name': u'target-3', 'sources': ['source-1', 'source-3']}, + {'id': 1, 'name': 'target-1', 'sources': ['source-2']}, + {'id': 2, 'name': 'target-2', 'sources': []}, + {'id': 3, 'name': 'target-3', 'sources': ['source-1', 'source-3']}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_update_with_invalid_null(self): - data = {'id': 1, 'name': u'source-1', 'target': None} + data = {'id': 1, 'name': 'source-1', 'target': None} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) + self.assertEquals(serializer.errors, {'target': ['Value may not be null']}) class SlugNullableForeignKeyTests(TestCase): @@ -166,28 +166,28 @@ class SlugNullableForeignKeyTests(TestCase): queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': 'target-1'}, - {'id': 2, 'name': u'source-2', 'target': 'target-1'}, - {'id': 3, 'name': u'source-3', 'target': None}, + {'id': 1, 'name': 'source-1', 'target': 'target-1'}, + {'id': 2, 'name': 'source-2', 'target': 'target-1'}, + {'id': 3, 'name': 'source-3', 'target': None}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_create_with_valid_null(self): - data = {'id': 4, 'name': u'source-4', 'target': None} + data = {'id': 4, 'name': 'source-4', 'target': None} serializer = NullableForeignKeySourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'source-4') + self.assertEqual(obj.name, 'source-4') # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': 'target-1'}, - {'id': 2, 'name': u'source-2', 'target': 'target-1'}, - {'id': 3, 'name': u'source-3', 'target': None}, - {'id': 4, 'name': u'source-4', 'target': None} + {'id': 1, 'name': 'source-1', 'target': 'target-1'}, + {'id': 2, 'name': 'source-2', 'target': 'target-1'}, + {'id': 3, 'name': 'source-3', 'target': None}, + {'id': 4, 'name': 'source-4', 'target': None} ] self.assertEquals(serializer.data, expected) @@ -196,27 +196,27 @@ class SlugNullableForeignKeyTests(TestCase): The emptystring should be interpreted as null in the context of relationships. """ - data = {'id': 4, 'name': u'source-4', 'target': ''} - expected_data = {'id': 4, 'name': u'source-4', 'target': None} + data = {'id': 4, 'name': 'source-4', 'target': ''} + expected_data = {'id': 4, 'name': 'source-4', 'target': None} serializer = NullableForeignKeySourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, expected_data) - self.assertEqual(obj.name, u'source-4') + self.assertEqual(obj.name, 'source-4') # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': 'target-1'}, - {'id': 2, 'name': u'source-2', 'target': 'target-1'}, - {'id': 3, 'name': u'source-3', 'target': None}, - {'id': 4, 'name': u'source-4', 'target': None} + {'id': 1, 'name': 'source-1', 'target': 'target-1'}, + {'id': 2, 'name': 'source-2', 'target': 'target-1'}, + {'id': 3, 'name': 'source-3', 'target': None}, + {'id': 4, 'name': 'source-4', 'target': None} ] self.assertEquals(serializer.data, expected) def test_foreign_key_update_with_valid_null(self): - data = {'id': 1, 'name': u'source-1', 'target': None} + data = {'id': 1, 'name': 'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) serializer = NullableForeignKeySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -227,9 +227,9 @@ class SlugNullableForeignKeyTests(TestCase): queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': None}, - {'id': 2, 'name': u'source-2', 'target': 'target-1'}, - {'id': 3, 'name': u'source-3', 'target': None} + {'id': 1, 'name': 'source-1', 'target': None}, + {'id': 2, 'name': 'source-2', 'target': 'target-1'}, + {'id': 3, 'name': 'source-3', 'target': None} ] self.assertEquals(serializer.data, expected) @@ -238,8 +238,8 @@ class SlugNullableForeignKeyTests(TestCase): The emptystring should be interpreted as null in the context of relationships. """ - data = {'id': 1, 'name': u'source-1', 'target': ''} - expected_data = {'id': 1, 'name': u'source-1', 'target': None} + data = {'id': 1, 'name': 'source-1', 'target': ''} + expected_data = {'id': 1, 'name': 'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) serializer = NullableForeignKeySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) @@ -250,8 +250,8 @@ class SlugNullableForeignKeyTests(TestCase): queryset = NullableForeignKeySource.objects.all() serializer = NullableForeignKeySourceSerializer(queryset) expected = [ - {'id': 1, 'name': u'source-1', 'target': None}, - {'id': 2, 'name': u'source-2', 'target': 'target-1'}, - {'id': 3, 'name': u'source-3', 'target': None} + {'id': 1, 'name': 'source-1', 'target': None}, + {'id': 2, 'name': 'source-2', 'target': 'target-1'}, + {'id': 3, 'name': 'source-3', 'target': None} ] self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index a00626b57..9697889db 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -236,17 +236,17 @@ class ValidationTests(TestCase): data = ['i am', 'a', 'list'] serializer = CommentSerializer(self.comment, data=data) self.assertEquals(serializer.is_valid(), False) - self.assertEquals(serializer.errors, {'non_field_errors': [u'Invalid data']}) + self.assertEquals(serializer.errors, {'non_field_errors': ['Invalid data']}) data = 'and i am a string' serializer = CommentSerializer(self.comment, data=data) self.assertEquals(serializer.is_valid(), False) - self.assertEquals(serializer.errors, {'non_field_errors': [u'Invalid data']}) + self.assertEquals(serializer.errors, {'non_field_errors': ['Invalid data']}) data = 42 serializer = CommentSerializer(self.comment, data=data) self.assertEquals(serializer.is_valid(), False) - self.assertEquals(serializer.errors, {'non_field_errors': [u'Invalid data']}) + self.assertEquals(serializer.errors, {'non_field_errors': ['Invalid data']}) def test_cross_field_validation(self): @@ -300,7 +300,7 @@ class ValidationTests(TestCase): } serializer = ActionItemSerializerCustomRestore(data=data) self.assertEquals(serializer.is_valid(), False) - self.assertEquals(serializer.errors, {'title': [u'Ensure this value has at most 200 characters (it has 201).']}) + self.assertEquals(serializer.errors, {'title': ['Ensure this value has at most 200 characters (it has 201).']}) def test_default_modelfield_max_length_exceeded(self): data = { @@ -340,7 +340,7 @@ class CustomValidationTests(TestCase): serializer = self.CommentSerializerWithFieldValidator(data=data) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'content': [u'Test not in value']}) + self.assertEquals(serializer.errors, {'content': ['Test not in value']}) def test_missing_data(self): """ @@ -352,7 +352,7 @@ class CustomValidationTests(TestCase): } serializer = self.CommentSerializerWithFieldValidator(data=incomplete_data) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'content': [u'This field is required.']}) + self.assertEquals(serializer.errors, {'content': ['This field is required.']}) def test_wrong_data(self): """ @@ -365,7 +365,7 @@ class CustomValidationTests(TestCase): } serializer = self.CommentSerializerWithFieldValidator(data=wrong_data) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'email': [u'Enter a valid e-mail address.']}) + self.assertEquals(serializer.errors, {'email': ['Enter a valid e-mail address.']}) class PositiveIntegerAsChoiceTests(TestCase): diff --git a/rest_framework/tests/utils.py b/rest_framework/tests/utils.py index 3906adb9a..4e6faac42 100644 --- a/rest_framework/tests/utils.py +++ b/rest_framework/tests/utils.py @@ -1,6 +1,6 @@ from django.test.client import RequestFactory, FakePayload from django.test.client import MULTIPART_CONTENT -from urlparse import urlparse +from rest_framework.compat import urlparse class RequestFactory(RequestFactory): @@ -14,7 +14,7 @@ class RequestFactory(RequestFactory): patch_data = self._encode_data(data, content_type) - parsed = urlparse(path) + parsed = urlparse.urlparse(path) r = { 'CONTENT_LENGTH': len(patch_data), 'CONTENT_TYPE': content_type, diff --git a/rest_framework/tests/validators.py b/rest_framework/tests/validators.py index c032985ec..8844cb74b 100644 --- a/rest_framework/tests/validators.py +++ b/rest_framework/tests/validators.py @@ -139,7 +139,7 @@ # raise errors on unexpected request data""" # content = {'qwerty': 'uiop', 'extra': 'extra'} # validator.allow_unknown_form_fields = True -# self.assertEqual({'qwerty': u'uiop'}, +# self.assertEqual({'qwerty': 'uiop'}, # validator.validate_request(content, None), # "Resource didn't accept unknown fields.") # validator.allow_unknown_form_fields = False diff --git a/tox.ini b/tox.ini index 22c85e493..542d5930f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,28 @@ [tox] downloadcache = {toxworkdir}/cache/ -envlist = py2.7-django1.5,py2.7-django1.4,py2.7-django1.3,py2.6-django1.5,py2.6-django1.4,py2.6-django1.3 +envlist = py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.7-django1.4,py2.7-django1.3,py2.6-django1.5,py2.6-django1.4,py2.6-django1.3 [testenv] commands = {envpython} rest_framework/runtests/runtests.py +[testenv:py3.3-django1.5] +basepython = python3.3 +deps = https://www.djangoproject.com/download/1.5c1/tarball/ + django-filter==0.5.4 + +[testenv:py3.2-django1.5] +basepython = python3.2 +deps = https://www.djangoproject.com/download/1.5c1/tarball/ + django-filter==0.5.4 + [testenv:py2.7-django1.5] basepython = python2.7 -deps = https://github.com/django/django/zipball/master +deps = https://www.djangoproject.com/download/1.5c1/tarball/ + django-filter==0.5.4 + +[testenv:py2.6-django1.5] +basepython = python2.6 +deps = https://www.djangoproject.com/download/1.5c1/tarball/ django-filter==0.5.4 [testenv:py2.7-django1.4] @@ -15,22 +30,19 @@ basepython = python2.7 deps = django==1.4.3 django-filter==0.5.4 -[testenv:py2.7-django1.3] -basepython = python2.7 -deps = django==1.3.5 - django-filter==0.5.4 - -[testenv:py2.6-django1.5] -basepython = python2.6 -deps = https://github.com/django/django/zipball/master - django-filter==0.5.4 - [testenv:py2.6-django1.4] basepython = python2.6 deps = django==1.4.3 django-filter==0.5.4 +[testenv:py2.7-django1.3] +basepython = python2.7 +deps = django==1.3.5 + django-filter==0.5.4 + six + [testenv:py2.6-django1.3] basepython = python2.6 deps = django==1.3.5 django-filter==0.5.4 + six From 00752dcd2a3647f2de2a259934753745597e3ade Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 1 Feb 2013 15:07:51 +0000 Subject: [PATCH 52/63] Py3k cleanup --- .travis.yml | 11 +- README.md | 2 +- docs/index.md | 2 +- rest_framework/compat.py | 4 +- rest_framework/request.py | 7 +- rest_framework/six.py | 389 +++++++++++++++++++++++++++++ rest_framework/utils/mediatypes.py | 3 +- tox.ini | 6 +- 8 files changed, 408 insertions(+), 16 deletions(-) create mode 100644 rest_framework/six.py diff --git a/.travis.yml b/.travis.yml index 572e483b9..662ea5c8b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,18 +4,17 @@ python: - "2.6" - "2.7" - "3.2" + - "3.3" env: - - DJANGO=https://github.com/django/django/zipball/master - DJANGO=https://www.djangoproject.com/download/1.5c1/tarball/ - DJANGO="django==1.4.3 --use-mirrors" - DJANGO="django==1.3.5 --use-mirrors" install: - pip install $DJANGO - - "if [[ $TRAVIS_PYTHON_VERSION != '3.2' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" - - "if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install https://github.com/alex/django-filter/tarball/master; fi" - - "if [[ $DJANGO == 'django==1.3.5 --use-mirrors' ]]; then pip install six --use-mirrors; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install https://github.com/alex/django-filter/tarball/master; fi" - export PYTHONPATH=. script: @@ -27,3 +26,7 @@ matrix: env: DJANGO="django==1.4.3 --use-mirrors" - python: "3.2" env: DJANGO="django==1.3.5 --use-mirrors" + - python: "3.3" + env: DJANGO="django==1.4.3 --use-mirrors" + - python: "3.3" + env: DJANGO="django==1.3.5 --use-mirrors" diff --git a/README.md b/README.md index 523b7e740..42c25c63e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ There is also a sandbox API you can use for testing purposes, [available here][s # Requirements -* Python (2.6, 2.7) +* Python (2.6, 2.7, 3.2, 3.3) * Django (1.3, 1.4, 1.5) **Optional:** diff --git a/docs/index.md b/docs/index.md index 453a67b8a..283ea069c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -33,7 +33,7 @@ There is also a sandbox API you can use for testing purposes, [available here][s REST framework requires the following: -* Python (2.6, 2.7) +* Python (2.6, 2.7, 3.2, 3.3) * Django (1.3, 1.4, 1.5) The following packages are optional: diff --git a/rest_framework/compat.py b/rest_framework/compat.py index ef11b85bb..8c64d9510 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -7,11 +7,11 @@ from __future__ import unicode_literals import django -# Try to import six from Django, fallback to six itself (1.3.x) +# Try to import six from Django, fallback to included `six`. try: from django.utils import six except: - import six + from rest_framework import six # location of patterns, url, include changes in 1.4 onwards try: diff --git a/rest_framework/request.py b/rest_framework/request.py index 23e1da87d..597892ef7 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -9,10 +9,11 @@ The wrapped request then offers a richer API, in particular : - full support of PUT method, including support for file uploads - form overloading of HTTP method, content type and content """ -from rest_framework.compat import BytesIO from django.http.multipartparser import parse_header +from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions +from rest_framework.compat import BytesIO from rest_framework.settings import api_settings @@ -20,7 +21,7 @@ def is_form_media_type(media_type): """ Return True if the media type is a valid form media type. """ - base_media_type, params = parse_header(media_type.encode('iso-8859-1')) + base_media_type, params = parse_header(media_type.encode(HTTP_HEADER_ENCODING)) return (base_media_type == 'application/x-www-form-urlencoded' or base_media_type == 'multipart/form-data') @@ -277,7 +278,7 @@ class Request(object): self._CONTENT_PARAM in self._data and self._CONTENTTYPE_PARAM in self._data): self._content_type = self._data[self._CONTENTTYPE_PARAM] - self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode('iso-8859-1')) + self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(HTTP_HEADER_ENCODING)) self._data, self._files = (Empty, Empty) def _parse(self): diff --git a/rest_framework/six.py b/rest_framework/six.py new file mode 100644 index 000000000..9e3823128 --- /dev/null +++ b/rest_framework/six.py @@ -0,0 +1,389 @@ +"""Utilities for writing code that runs on Python 2 and 3""" + +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.2.0" + + +# True if we are running on Python 3. +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform == "java": + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) + # This is a bit ugly, but it avoids running this again. + delattr(tp, self.name) + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + + +class _MovedItems(types.ModuleType): + """Lazy loading of moved objects""" + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("reload_module", "__builtin__", "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("winreg", "_winreg"), +] +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) +del attr + +moves = sys.modules["django.utils.six.moves"] = _MovedItems("moves") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_code = "__code__" + _func_defaults = "__defaults__" + + _iterkeys = "keys" + _itervalues = "values" + _iteritems = "items" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_code = "func_code" + _func_defaults = "func_defaults" + + _iterkeys = "iterkeys" + _itervalues = "itervalues" + _iteritems = "iteritems" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +if PY3: + def get_unbound_function(unbound): + return unbound + + Iterator = object + + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) +else: + def get_unbound_function(unbound): + return unbound.im_func + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) + + +def iterkeys(d): + """Return an iterator over the keys of a dictionary.""" + return iter(getattr(d, _iterkeys)()) + +def itervalues(d): + """Return an iterator over the values of a dictionary.""" + return iter(getattr(d, _itervalues)()) + +def iteritems(d): + """Return an iterator over the (key, value) pairs of a dictionary.""" + return iter(getattr(d, _iteritems)()) + + +if PY3: + def b(s): + return s.encode("latin-1") + def u(s): + return s + if sys.version_info[1] <= 1: + def int2byte(i): + return bytes((i,)) + else: + # This is about 2x faster than the implementation above on 3.2+ + int2byte = operator.methodcaller("to_bytes", 1, "big") + import io + StringIO = io.StringIO + BytesIO = io.BytesIO +else: + def b(s): + return s + def u(s): + return unicode(s, "unicode_escape") + int2byte = chr + import StringIO + StringIO = BytesIO = StringIO.StringIO +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +if PY3: + import builtins + exec_ = getattr(builtins, "exec") + + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + + print_ = getattr(builtins, "print") + del builtins + +else: + def exec_(code, globs=None, locs=None): + """Execute code in a namespace.""" + if globs is None: + frame = sys._getframe(1) + globs = frame.f_globals + if locs is None: + locs = frame.f_locals + del frame + elif locs is None: + locs = globs + exec("""exec code in globs, locs""") + + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + + def print_(*args, **kwargs): + """The new-style print function.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + def write(data): + if not isinstance(data, basestring): + data = str(data) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) + +_add_doc(reraise, """Reraise an exception.""") + + +def with_metaclass(meta, base=object): + """Create a base class with a metaclass.""" + return meta("NewBase", (base,), {}) + + +### Additional customizations for Django ### + +if PY3: + _iterlists = "lists" + _assertRaisesRegex = "assertRaisesRegex" +else: + _iterlists = "iterlists" + _assertRaisesRegex = "assertRaisesRegexp" + + +def iterlists(d): + """Return an iterator over the values of a MultiValueDict.""" + return getattr(d, _iterlists)() + + +def assertRaisesRegex(self, *args, **kwargs): + return getattr(self, _assertRaisesRegex)(*args, **kwargs) + + +add_move(MovedModule("_dummy_thread", "dummy_thread")) +add_move(MovedModule("_thread", "thread")) diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py index 3fc59eddd..aea1b6292 100644 --- a/rest_framework/utils/mediatypes.py +++ b/rest_framework/utils/mediatypes.py @@ -5,6 +5,7 @@ See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 """ from django.http.multipartparser import parse_header +from rest_framework import HTTP_HEADER_ENCODING def media_type_matches(lhs, rhs): @@ -47,7 +48,7 @@ class _MediaType(object): if media_type_str is None: media_type_str = '' self.orig = media_type_str - self.full_type, self.params = parse_header(media_type_str.encode('iso-8859-1')) + self.full_type, self.params = parse_header(media_type_str.encode(HTTP_HEADER_ENCODING)) self.main_type, sep, self.sub_type = self.full_type.partition('/') def match(self, other): diff --git a/tox.ini b/tox.ini index 542d5930f..05733efe1 100644 --- a/tox.ini +++ b/tox.ini @@ -8,12 +8,12 @@ commands = {envpython} rest_framework/runtests/runtests.py [testenv:py3.3-django1.5] basepython = python3.3 deps = https://www.djangoproject.com/download/1.5c1/tarball/ - django-filter==0.5.4 + https://github.com/alex/django-filter/archive/master.tar.gz [testenv:py3.2-django1.5] basepython = python3.2 deps = https://www.djangoproject.com/download/1.5c1/tarball/ - django-filter==0.5.4 + https://github.com/alex/django-filter/archive/master.tar.gz [testenv:py2.7-django1.5] basepython = python2.7 @@ -39,10 +39,8 @@ deps = django==1.4.3 basepython = python2.7 deps = django==1.3.5 django-filter==0.5.4 - six [testenv:py2.6-django1.3] basepython = python2.6 deps = django==1.3.5 django-filter==0.5.4 - six From b9f1fbb5d2a3a303968d3afbe72751219583b28b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 1 Feb 2013 15:10:52 +0000 Subject: [PATCH 53/63] Added @xordoquy for the incredible py3k work! Commiter number 100! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index a67a81690..ebca44916 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -101,6 +101,7 @@ The following people have helped make REST framework great. * MichaƂ Jaworski - [swistakm] * Andrea de Marco - [z4r] * Fernando Rocha - [fernandogrd] +* Xavier Ordoquy - [xordoquy] Many thanks to everyone who's contributed to the project. @@ -237,3 +238,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [swistakm]: https://github.com/swistakm [z4r]: https://github.com/z4r [fernandogrd]: https://github.com/fernandogrd +[xordoquy]: https://github.com/xordoquy From 2c634c0e5cd03cb47674b0d4b76bd7494e030e36 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 4 Feb 2013 19:51:31 +0000 Subject: [PATCH 54/63] Use request.QUERY_PARAMS internally (instead of request.GET) --- rest_framework/filters.py | 2 +- rest_framework/negotiation.py | 4 ++-- rest_framework/renderers.py | 2 +- rest_framework/tests/negotiation.py | 8 +++++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index bcc876607..f7b5a1bc1 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -54,6 +54,6 @@ class DjangoFilterBackend(BaseFilterBackend): filter_class = self.get_filter_class(view) if filter_class: - return filter_class(request.GET, queryset=queryset) + return filter_class(request.QUERY_PARAMS, queryset=queryset) return queryset diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index ee2800a6e..0a7b6db64 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -33,7 +33,7 @@ class DefaultContentNegotiation(BaseContentNegotiation): """ # Allow URL style format override. eg. "?format=json format_query_param = self.settings.URL_FORMAT_OVERRIDE - format = format_suffix or request.GET.get(format_query_param) + format = format_suffix or request.QUERY_PARAMS.get(format_query_param) if format: renderers = self.filter_renderers(renderers, format) @@ -80,5 +80,5 @@ class DefaultContentNegotiation(BaseContentNegotiation): Allows URL style accept override. eg. "?accept=application/json" """ header = request.META.get('HTTP_ACCEPT', '*/*') - header = request.GET.get(self.settings.URL_ACCEPT_OVERRIDE, header) + header = request.QUERY_PARAMS.get(self.settings.URL_ACCEPT_OVERRIDE, header) return [token.strip() for token in header.split(',')] diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index b3ee06902..7eb6068a3 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -88,7 +88,7 @@ class JSONPRenderer(JSONRenderer): Determine the name of the callback to wrap around the json output. """ request = renderer_context.get('request', None) - params = request and request.GET or {} + params = request and request.QUERY_PARAMS or {} return params.get(self.callback_parameter, self.default_callback) def render(self, data, accepted_media_type=None, renderer_context=None): diff --git a/rest_framework/tests/negotiation.py b/rest_framework/tests/negotiation.py index e06354ead..7706908b8 100644 --- a/rest_framework/tests/negotiation.py +++ b/rest_framework/tests/negotiation.py @@ -1,6 +1,8 @@ from django.test import TestCase from django.test.client import RequestFactory from rest_framework.negotiation import DefaultContentNegotiation +from rest_framework.request import Request + factory = RequestFactory() @@ -22,16 +24,16 @@ class TestAcceptedMediaType(TestCase): return self.negotiator.select_renderer(request, self.renderers) def test_client_without_accept_use_renderer(self): - request = factory.get('/') + request = Request(factory.get('/')) accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEquals(accepted_media_type, 'application/json') def test_client_underspecifies_accept_use_renderer(self): - request = factory.get('/', HTTP_ACCEPT='*/*') + request = Request(factory.get('/', HTTP_ACCEPT='*/*')) accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEquals(accepted_media_type, 'application/json') def test_client_overspecifies_accept_use_client(self): - request = factory.get('/', HTTP_ACCEPT='application/json; indent=8') + request = Request(factory.get('/', HTTP_ACCEPT='application/json; indent=8')) accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEquals(accepted_media_type, 'application/json; indent=8') From 97f2b994951605ffdef08159be450d1e77762bf9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 4 Feb 2013 19:51:50 +0000 Subject: [PATCH 55/63] Don't use deprecated xml style --- rest_framework/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 4a2b34a5c..b601156ba 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -149,7 +149,7 @@ class XMLParser(BaseParser): convert the xml `element` into the corresponding python object """ - children = element.getchildren() + children = list(element) if len(children) == 0: return self._type_convert(element.text) From b82227e517bb7baced840b29d04a545a7b7557ae Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 4 Feb 2013 20:38:18 +0000 Subject: [PATCH 56/63] remove broken import --- rest_framework/relations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 93f19362a..a946d961e 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -9,7 +9,6 @@ from django.forms.models import ModelChoiceIterator from django.utils.translation import ugettext_lazy as _ from rest_framework.fields import Field, WritableField from rest_framework.reverse import reverse -from urlparse import urlparse from rest_framework.compat import urlparse from rest_framework.compat import smart_text import warnings From b052c92ac38f90e5b56cfd128cd4a488713c048e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 4 Feb 2013 20:55:35 +0000 Subject: [PATCH 57/63] Cleanup imports Mostly adding `from __future__ import unicode_literals` everywhere. --- rest_framework/authentication.py | 3 - rest_framework/decorators.py | 4 +- rest_framework/exceptions.py | 1 + rest_framework/filters.py | 1 + rest_framework/generics.py | 2 +- rest_framework/negotiation.py | 1 + rest_framework/pagination.py | 1 + rest_framework/parsers.py | 2 +- rest_framework/permissions.py | 2 +- rest_framework/relations.py | 2 - rest_framework/request.py | 2 +- rest_framework/response.py | 2 +- rest_framework/reverse.py | 1 + rest_framework/serializers.py | 1 + rest_framework/settings.py | 1 + rest_framework/status.py | 1 + rest_framework/templatetags/rest_framework.py | 2 - rest_framework/tests/authentication.py | 2 - rest_framework/tests/breadcrumbs.py | 1 + rest_framework/tests/decorators.py | 1 + rest_framework/tests/description.py | 1 + rest_framework/tests/fields.py | 2 +- rest_framework/tests/files.py | 5 +- rest_framework/tests/filterset.py | 1 + rest_framework/tests/genericrelations.py | 1 - rest_framework/tests/htmlrenderer.py | 1 + .../tests/hyperlinkedserializers.py | 1 + rest_framework/tests/models.py | 31 +--- rest_framework/tests/modelviews.py | 90 ------------ rest_framework/tests/negotiation.py | 1 + rest_framework/tests/pagination.py | 1 + rest_framework/tests/parsers.py | 134 +----------------- rest_framework/tests/relations.py | 2 +- rest_framework/tests/relations_hyperlink.py | 1 - rest_framework/tests/relations_pk.py | 2 - rest_framework/tests/request.py | 3 +- rest_framework/tests/response.py | 1 + rest_framework/tests/reverse.py | 1 + rest_framework/tests/serializer.py | 5 +- rest_framework/tests/settings.py | 1 + rest_framework/tests/status.py | 1 + rest_framework/tests/testcases.py | 1 + rest_framework/tests/tests.py | 1 + rest_framework/tests/throttling.py | 3 +- rest_framework/tests/urlpatterns.py | 4 +- rest_framework/tests/utils.py | 1 + rest_framework/tests/views.py | 3 +- rest_framework/throttling.py | 3 +- rest_framework/urlpatterns.py | 3 +- rest_framework/urls.py | 1 + rest_framework/utils/__init__.py | 1 + rest_framework/utils/breadcrumbs.py | 1 + rest_framework/utils/encoders.py | 7 +- rest_framework/utils/mediatypes.py | 2 +- rest_framework/views.py | 4 +- 55 files changed, 58 insertions(+), 296 deletions(-) delete mode 100644 rest_framework/tests/modelviews.py diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index c15568db4..14b2136b3 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -2,14 +2,11 @@ Provides a set of pluggable authentication policies. """ from __future__ import unicode_literals - from django.contrib.auth import authenticate from django.utils.encoding import DjangoUnicodeDecodeError from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework.compat import CsrfViewMiddleware -from rest_framework.compat import smart_text from rest_framework.authtoken.models import Token -from rest_framework.settings import api_settings import base64 diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 7a4103e16..8250cd3ba 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals +from rest_framework.compat import six from rest_framework.views import APIView import types @@ -12,7 +14,7 @@ def api_view(http_method_names): def decorator(func): WrappedAPIView = type( - 'WrappedAPIView', + six.PY3 and 'WrappedAPIView' or b'WrappedAPIView', (APIView,), {'__doc__': func.__doc__} ) diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index d635351c6..0c96ecdd5 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -4,6 +4,7 @@ Handled exceptions raised by REST framework. In addition Django's built in 403 and 404 exceptions are handled. (`django.http.Http404` and `django.core.exceptions.PermissionDenied`) """ +from __future__ import unicode_literals from rest_framework import status diff --git a/rest_framework/filters.py b/rest_framework/filters.py index f7b5a1bc1..6fea46faf 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from rest_framework.compat import django_filters FilterSet = django_filters and django_filters.FilterSet or None diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 19f2b7047..9e9319176 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -1,7 +1,7 @@ """ Generic views that provide commonly needed behaviour. """ - +from __future__ import unicode_literals from rest_framework import views, mixins from rest_framework.settings import api_settings from django.views.generic.detail import SingleObjectMixin diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index 0a7b6db64..0694d35f5 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from django.http import Http404 from rest_framework import exceptions from rest_framework.settings import api_settings diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 92d41e0e2..03a7a30f8 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from rest_framework import serializers from rest_framework.templatetags.rest_framework import replace_query_param diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index b601156ba..26009cdf1 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -4,7 +4,7 @@ Parsers are used to parse the content of incoming HTTP requests. They give us a generic way of being able to handle various media types on the request, such as form content or json encoded data. """ - +from __future__ import unicode_literals from django.http import QueryDict from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 655b78a34..3222dbf22 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -1,7 +1,7 @@ """ Provides a set of pluggable permission policies. """ - +from __future__ import unicode_literals SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] diff --git a/rest_framework/relations.py b/rest_framework/relations.py index a946d961e..ae4ef6b32 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -1,6 +1,4 @@ - from __future__ import unicode_literals - from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.urlresolvers import resolve, get_script_prefix from django import forms diff --git a/rest_framework/request.py b/rest_framework/request.py index 597892ef7..16f47d16b 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -9,7 +9,7 @@ The wrapped request then offers a richer API, in particular : - full support of PUT method, including support for file uploads - form overloading of HTTP method, content type and content """ - +from __future__ import unicode_literals from django.http.multipartparser import parse_header from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions diff --git a/rest_framework/response.py b/rest_framework/response.py index 0a484c4a1..5e1bf46e2 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -1,6 +1,6 @@ +from __future__ import unicode_literals from django.core.handlers.wsgi import STATUS_CODE_TEXT from django.template.response import SimpleTemplateResponse - from rest_framework.compat import six diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py index c9db02f06..a51b07f54 100644 --- a/rest_framework/reverse.py +++ b/rest_framework/reverse.py @@ -1,6 +1,7 @@ """ Provide reverse functions that return fully qualified URLs """ +from __future__ import unicode_literals from django.core.urlresolvers import reverse as django_reverse from django.utils.functional import lazy diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c5b3494cf..b635d20d3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals import copy import datetime import types diff --git a/rest_framework/settings.py b/rest_framework/settings.py index b3ca01347..b7aa0bbe0 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -17,6 +17,7 @@ This module provides the `api_setting` object, that is used to access REST framework settings, checking for user settings first, then falling back to the defaults. """ +from __future__ import unicode_literals from django.conf import settings from django.utils import importlib from rest_framework.compat import six diff --git a/rest_framework/status.py b/rest_framework/status.py index a1eb48dae..b9f249f9f 100644 --- a/rest_framework/status.py +++ b/rest_framework/status.py @@ -4,6 +4,7 @@ Descriptive HTTP status codes, for code readability. See RFC 2616 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html And RFC 6585 - http://tools.ietf.org/html/rfc6585 """ +from __future__ import unicode_literals HTTP_100_CONTINUE = 100 HTTP_101_SWITCHING_PROTOCOLS = 101 diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index cbafbe0e6..a1db65bc6 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -1,5 +1,4 @@ from __future__ import unicode_literals, absolute_import - from django import template from django.core.urlresolvers import reverse from django.http import QueryDict @@ -8,7 +7,6 @@ from django.utils.safestring import SafeData, mark_safe from rest_framework.compat import urlparse from rest_framework.compat import force_text from rest_framework.compat import six - import re import string diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 7dde6d22a..c9df17330 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -1,5 +1,4 @@ from __future__ import unicode_literals - from django.contrib.auth.models import User from django.http import HttpResponse from django.test import Client, TestCase @@ -9,7 +8,6 @@ from rest_framework.authtoken.models import Token from rest_framework.authentication import TokenAuthentication, BasicAuthentication, SessionAuthentication from rest_framework.compat import patterns from rest_framework.views import APIView - import json import base64 diff --git a/rest_framework/tests/breadcrumbs.py b/rest_framework/tests/breadcrumbs.py index df8916832..d9ed647e2 100644 --- a/rest_framework/tests/breadcrumbs.py +++ b/rest_framework/tests/breadcrumbs.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from django.test import TestCase from rest_framework.compat import patterns, url from rest_framework.utils.breadcrumbs import get_breadcrumbs diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py index 82f912e98..a11af3a5d 100644 --- a/rest_framework/tests/decorators.py +++ b/rest_framework/tests/decorators.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from django.test import TestCase from rest_framework import status from rest_framework.response import Response diff --git a/rest_framework/tests/description.py b/rest_framework/tests/description.py index d958b8405..20963a9cf 100644 --- a/rest_framework/tests/description.py +++ b/rest_framework/tests/description.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from django.test import TestCase from rest_framework.views import APIView from rest_framework.compat import apply_markdown diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 8068272d4..b7587bf14 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -1,7 +1,7 @@ """ General serializer field tests. """ - +from __future__ import unicode_literals from django.db import models from django.test import TestCase from rest_framework import serializers diff --git a/rest_framework/tests/files.py b/rest_framework/tests/files.py index 0434f9009..ce00ea6ba 100644 --- a/rest_framework/tests/files.py +++ b/rest_framework/tests/files.py @@ -1,10 +1,9 @@ -import datetime - +from __future__ import unicode_literals from django.test import TestCase - from rest_framework import serializers from rest_framework.compat import BytesIO from rest_framework.compat import six +import datetime class UploadedFile(object): diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py index af2e6c2e7..daea6e53d 100644 --- a/rest_framework/tests/filterset.py +++ b/rest_framework/tests/filterset.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals import datetime from decimal import Decimal from django.test import TestCase diff --git a/rest_framework/tests/genericrelations.py b/rest_framework/tests/genericrelations.py index 91a986043..88d4efa32 100644 --- a/rest_framework/tests/genericrelations.py +++ b/rest_framework/tests/genericrelations.py @@ -1,5 +1,4 @@ from __future__ import unicode_literals - from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey from django.db import models diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py index 34caa208b..702e80246 100644 --- a/rest_framework/tests/htmlrenderer.py +++ b/rest_framework/tests/htmlrenderer.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from django.core.exceptions import PermissionDenied from django.http import Http404 from django.test import TestCase diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index c6a8224b1..bc9b87695 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals import json from django.test import TestCase from django.test.client import RequestFactory diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 9ab153280..f2117538c 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -1,35 +1,6 @@ +from __future__ import unicode_literals from django.db import models -from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes.generic import GenericForeignKey, GenericRelation -# from django.contrib.auth.models import Group - - -# class CustomUser(models.Model): -# """ -# A custom user model, which uses a 'through' table for the foreign key -# """ -# username = models.CharField(max_length=255, unique=True) -# groups = models.ManyToManyField( -# to=Group, blank=True, null=True, through='UserGroupMap' -# ) - -# @models.permalink -# def get_absolute_url(self): -# return ('custom_user', (), { -# 'pk': self.id -# }) - - -# class UserGroupMap(models.Model): -# user = models.ForeignKey(to=CustomUser) -# group = models.ForeignKey(to=Group) - -# @models.permalink -# def get_absolute_url(self): -# return ('user_group_map', (), { -# 'pk': self.id -# }) def foobar(): return 'foobar' diff --git a/rest_framework/tests/modelviews.py b/rest_framework/tests/modelviews.py deleted file mode 100644 index f12e3b979..000000000 --- a/rest_framework/tests/modelviews.py +++ /dev/null @@ -1,90 +0,0 @@ -# from rest_framework.compat import patterns, url -# from django.forms import ModelForm -# from django.contrib.auth.models import Group, User -# from rest_framework.resources import ModelResource -# from rest_framework.views import ListOrCreateModelView, InstanceModelView -# from rest_framework.tests.models import CustomUser -# from rest_framework.tests.testcases import TestModelsTestCase - - -# class GroupResource(ModelResource): -# model = Group - - -# class UserForm(ModelForm): -# class Meta: -# model = User -# exclude = ('last_login', 'date_joined') - - -# class UserResource(ModelResource): -# model = User -# form = UserForm - - -# class CustomUserResource(ModelResource): -# model = CustomUser - -# urlpatterns = patterns('', -# url(r'^users/$', ListOrCreateModelView.as_view(resource=UserResource), name='users'), -# url(r'^users/(?P[0-9]+)/$', InstanceModelView.as_view(resource=UserResource)), -# url(r'^customusers/$', ListOrCreateModelView.as_view(resource=CustomUserResource), name='customusers'), -# url(r'^customusers/(?P[0-9]+)/$', InstanceModelView.as_view(resource=CustomUserResource)), -# url(r'^groups/$', ListOrCreateModelView.as_view(resource=GroupResource), name='groups'), -# url(r'^groups/(?P[0-9]+)/$', InstanceModelView.as_view(resource=GroupResource)), -# ) - - -# class ModelViewTests(TestModelsTestCase): -# """Test the model views rest_framework provides""" -# urls = 'rest_framework.tests.modelviews' - -# def test_creation(self): -# """Ensure that a model object can be created""" -# self.assertEqual(0, Group.objects.count()) - -# response = self.client.post('/groups/', {'name': 'foo'}) - -# self.assertEqual(response.status_code, 201) -# self.assertEqual(1, Group.objects.count()) -# self.assertEqual('foo', Group.objects.all()[0].name) - -# def test_creation_with_m2m_relation(self): -# """Ensure that a model object with a m2m relation can be created""" -# group = Group(name='foo') -# group.save() -# self.assertEqual(0, User.objects.count()) - -# response = self.client.post('/users/', {'username': 'bar', 'password': 'baz', 'groups': [group.id]}) - -# self.assertEqual(response.status_code, 201) -# self.assertEqual(1, User.objects.count()) - -# user = User.objects.all()[0] -# self.assertEqual('bar', user.username) -# self.assertEqual('baz', user.password) -# self.assertEqual(1, user.groups.count()) - -# group = user.groups.all()[0] -# self.assertEqual('foo', group.name) - -# def test_creation_with_m2m_relation_through(self): -# """ -# Ensure that a model object with a m2m relation can be created where that -# relation uses a through table -# """ -# group = Group(name='foo') -# group.save() -# self.assertEqual(0, User.objects.count()) - -# response = self.client.post('/customusers/', {'username': 'bar', 'groups': [group.id]}) - -# self.assertEqual(response.status_code, 201) -# self.assertEqual(1, CustomUser.objects.count()) - -# user = CustomUser.objects.all()[0] -# self.assertEqual('bar', user.username) -# self.assertEqual(1, user.groups.count()) - -# group = user.groups.all()[0] -# self.assertEqual('foo', group.name) diff --git a/rest_framework/tests/negotiation.py b/rest_framework/tests/negotiation.py index 7706908b8..5769dd5ff 100644 --- a/rest_framework/tests/negotiation.py +++ b/rest_framework/tests/negotiation.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from django.test import TestCase from django.test.client import RequestFactory from rest_framework.negotiation import DefaultContentNegotiation diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 697dfb5bd..b85ce1448 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals import datetime from decimal import Decimal from django.core.paginator import Paginator diff --git a/rest_framework/tests/parsers.py b/rest_framework/tests/parsers.py index ffa39b1f3..c03df08f9 100644 --- a/rest_framework/tests/parsers.py +++ b/rest_framework/tests/parsers.py @@ -1,136 +1,4 @@ -# """ -# .. -# >>> from rest_framework.parsers import FormParser -# >>> from django.test.client import RequestFactory -# >>> from rest_framework.views import View -# >>> from StringIO import StringIO -# >>> from urllib import urlencode -# >>> req = RequestFactory().get('/') -# >>> some_view = View() -# >>> some_view.request = req # Make as if this request had been dispatched -# -# FormParser -# ============ -# -# Data flatening -# ---------------- -# -# Here is some example data, which would eventually be sent along with a post request : -# -# >>> inpt = urlencode([ -# ... ('key1', 'bla1'), -# ... ('key2', 'blo1'), ('key2', 'blo2'), -# ... ]) -# -# Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter : -# -# >>> (data, files) = FormParser(some_view).parse(StringIO(inpt)) -# >>> data == {'key1': 'bla1', 'key2': 'blo1'} -# True -# -# However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` : -# -# >>> class MyFormParser(FormParser): -# ... -# ... def is_a_list(self, key, val_list): -# ... return len(val_list) > 1 -# -# This new parser only flattens the lists of parameters that contain a single value. -# -# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt)) -# >>> data == {'key1': 'bla1', 'key2': ['blo1', 'blo2']} -# True -# -# .. note:: The same functionality is available for :class:`parsers.MultiPartParser`. -# -# Submitting an empty list -# -------------------------- -# -# When submitting an empty select multiple, like this one :: -# -# -# -# The browsers usually strip the parameter completely. A hack to avoid this, and therefore being able to submit an empty select multiple, is to submit a value that tells the server that the list is empty :: -# -# -# -# :class:`parsers.FormParser` provides the server-side implementation for this hack. Considering the following posted data : -# -# >>> inpt = urlencode([ -# ... ('key1', 'blo1'), ('key1', '_empty'), -# ... ('key2', '_empty'), -# ... ]) -# -# :class:`parsers.FormParser` strips the values ``_empty`` from all the lists. -# -# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt)) -# >>> data == {'key1': 'blo1'} -# True -# -# Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it. -# -# >>> class MyFormParser(FormParser): -# ... -# ... def is_a_list(self, key, val_list): -# ... return key == 'key2' -# ... -# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt)) -# >>> data == {'key1': 'blo1', 'key2': []} -# True -# -# Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`. -# """ -# import httplib, mimetypes -# from tempfile import TemporaryFile -# from django.test import TestCase -# from django.test.client import RequestFactory -# from rest_framework.parsers import MultiPartParser -# from rest_framework.views import View -# from StringIO import StringIO -# -# def encode_multipart_formdata(fields, files): -# """For testing multipart parser. -# fields is a sequence of (name, value) elements for regular form fields. -# files is a sequence of (name, filename, value) elements for data to be uploaded as files -# Return (content_type, body).""" -# BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' -# CRLF = '\r\n' -# L = [] -# for (key, value) in fields: -# L.append('--' + BOUNDARY) -# L.append('Content-Disposition: form-data; name="%s"' % key) -# L.append('') -# L.append(value) -# for (key, filename, value) in files: -# L.append('--' + BOUNDARY) -# L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) -# L.append('Content-Type: %s' % get_content_type(filename)) -# L.append('') -# L.append(value) -# L.append('--' + BOUNDARY + '--') -# L.append('') -# body = CRLF.join(L) -# content_type = 'multipart/form-data; boundary=%s' % BOUNDARY -# return content_type, body -# -# def get_content_type(filename): -# return mimetypes.guess_type(filename)[0] or 'application/octet-stream' -# -#class TestMultiPartParser(TestCase): -# def setUp(self): -# self.req = RequestFactory() -# self.content_type, self.body = encode_multipart_formdata([('key1', 'val1'), ('key1', 'val2')], -# [('file1', 'pic.jpg', 'blablabla'), ('file1', 't.txt', 'blobloblo')]) -# -# def test_multipartparser(self): -# """Ensure that MultiPartParser can parse multipart/form-data that contains a mix of several files and parameters.""" -# post_req = RequestFactory().post('/', self.body, content_type=self.content_type) -# view = View() -# view.request = post_req -# (data, files) = MultiPartParser(view).parse(StringIO(self.body)) -# self.assertEqual(data['key1'], 'val1') -# self.assertEqual(files['file1'].read(), 'blablabla') - +from __future__ import unicode_literals from rest_framework.compat import StringIO from django import forms from django.test import TestCase diff --git a/rest_framework/tests/relations.py b/rest_framework/tests/relations.py index edc85f9ea..5fc320380 100644 --- a/rest_framework/tests/relations.py +++ b/rest_framework/tests/relations.py @@ -1,7 +1,7 @@ """ General tests for relational fields. """ - +from __future__ import unicode_literals from django.db import models from django.test import TestCase from rest_framework import serializers diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index 76e314767..4fbf0b639 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -1,5 +1,4 @@ from __future__ import unicode_literals - from django.test import TestCase from rest_framework import serializers from rest_framework.compat import patterns, url diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index ca7ac17e7..ffd1127ea 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -1,6 +1,4 @@ from __future__ import unicode_literals - -from django.db import models from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index 92b1bfd8d..9d4fdc7bd 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -1,7 +1,7 @@ """ Tests for content parsing, and form-overloaded content parsing. """ -import json +from __future__ import unicode_literals from django.contrib.auth.models import User from django.contrib.auth import authenticate, login, logout from django.contrib.sessions.middleware import SessionMiddleware @@ -21,6 +21,7 @@ from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import APIView from rest_framework.compat import six +import json factory = RequestFactory() diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py index 453488d00..3e1da9051 100644 --- a/rest_framework/tests/response.py +++ b/rest_framework/tests/response.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from django.test import TestCase from rest_framework.compat import patterns, url, include from rest_framework.response import Response diff --git a/rest_framework/tests/reverse.py b/rest_framework/tests/reverse.py index 8c86e1fbe..4ad4d684f 100644 --- a/rest_framework/tests/reverse.py +++ b/rest_framework/tests/reverse.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from django.test import TestCase from django.test.client import RequestFactory from rest_framework.compat import patterns, url diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 1f46cfc7a..dda388aa1 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1,13 +1,12 @@ from __future__ import unicode_literals - -import datetime -import pickle from django.utils.datastructures import MultiValueDict from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo) +import datetime +import pickle class SubComment(object): diff --git a/rest_framework/tests/settings.py b/rest_framework/tests/settings.py index 0293fdc3e..857375c21 100644 --- a/rest_framework/tests/settings.py +++ b/rest_framework/tests/settings.py @@ -1,4 +1,5 @@ """Tests for the settings module""" +from __future__ import unicode_literals from django.test import TestCase from rest_framework.settings import APISettings, DEFAULTS, IMPORT_STRINGS diff --git a/rest_framework/tests/status.py b/rest_framework/tests/status.py index 30df5cef3..c0d11b5f2 100644 --- a/rest_framework/tests/status.py +++ b/rest_framework/tests/status.py @@ -1,4 +1,5 @@ """Tests for the status module""" +from __future__ import unicode_literals from django.test import TestCase from rest_framework import status diff --git a/rest_framework/tests/testcases.py b/rest_framework/tests/testcases.py index 97f492ff4..f8c2579e6 100644 --- a/rest_framework/tests/testcases.py +++ b/rest_framework/tests/testcases.py @@ -1,4 +1,5 @@ # http://djangosnippets.org/snippets/1011/ +from __future__ import unicode_literals from django.conf import settings from django.core.management import call_command from django.db.models import loading diff --git a/rest_framework/tests/tests.py b/rest_framework/tests/tests.py index adeaf6da3..08f88e113 100644 --- a/rest_framework/tests/tests.py +++ b/rest_framework/tests/tests.py @@ -2,6 +2,7 @@ Force import of all modules in this package in order to get the standard test runner to pick up the tests. Yowzers. """ +from __future__ import unicode_literals import os modules = [filename.rsplit('.', 1)[0] diff --git a/rest_framework/tests/throttling.py b/rest_framework/tests/throttling.py index 4b98b9414..4616f3254 100644 --- a/rest_framework/tests/throttling.py +++ b/rest_framework/tests/throttling.py @@ -1,11 +1,10 @@ """ Tests for the throttling implementations in the permissions module. """ - +from __future__ import unicode_literals from django.test import TestCase from django.contrib.auth.models import User from django.core.cache import cache - from django.test.client import RequestFactory from rest_framework.views import APIView from rest_framework.throttling import UserRateThrottle diff --git a/rest_framework/tests/urlpatterns.py b/rest_framework/tests/urlpatterns.py index 43e8ef692..41245ad15 100644 --- a/rest_framework/tests/urlpatterns.py +++ b/rest_framework/tests/urlpatterns.py @@ -1,10 +1,8 @@ +from __future__ import unicode_literals from collections import namedtuple - from django.core import urlresolvers - from django.test import TestCase from django.test.client import RequestFactory - from rest_framework.compat import patterns, url, include from rest_framework.urlpatterns import format_suffix_patterns diff --git a/rest_framework/tests/utils.py b/rest_framework/tests/utils.py index 4e6faac42..224c4f9d3 100644 --- a/rest_framework/tests/utils.py +++ b/rest_framework/tests/utils.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from django.test.client import RequestFactory, FakePayload from django.test.client import MULTIPART_CONTENT from rest_framework.compat import urlparse diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py index f24325168..7063c3fbe 100644 --- a/rest_framework/tests/views.py +++ b/rest_framework/tests/views.py @@ -1,6 +1,4 @@ from __future__ import unicode_literals - -import copy from django.test import TestCase from django.test.client import RequestFactory from rest_framework import status @@ -8,6 +6,7 @@ from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import APIView +import copy factory = RequestFactory() diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 8fe64248d..810cad637 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -1,7 +1,8 @@ -import time +from __future__ import unicode_literals from django.core.cache import cache from rest_framework import exceptions from rest_framework.settings import api_settings +import time class BaseThrottle(object): diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 47789026f..d9143bb4c 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -1,6 +1,7 @@ +from __future__ import unicode_literals +from django.core.urlresolvers import RegexURLResolver from rest_framework.compat import url, include from rest_framework.settings import api_settings -from django.core.urlresolvers import RegexURLResolver def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required): diff --git a/rest_framework/urls.py b/rest_framework/urls.py index fbe4bc07e..9c4719f1d 100644 --- a/rest_framework/urls.py +++ b/rest_framework/urls.py @@ -12,6 +12,7 @@ your authentication settings include `SessionAuthentication`. url(r'^auth', include('rest_framework.urls', namespace='rest_framework')) ) """ +from __future__ import unicode_literals from rest_framework.compat import patterns, url diff --git a/rest_framework/utils/__init__.py b/rest_framework/utils/__init__.py index 1603f9725..3bab3b5fa 100644 --- a/rest_framework/utils/__init__.py +++ b/rest_framework/utils/__init__.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from django.utils.xmlutils import SimplerXMLGenerator from rest_framework.compat import StringIO from rest_framework.compat import six diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py index 80e39d46d..af21ac79b 100644 --- a/rest_framework/utils/breadcrumbs.py +++ b/rest_framework/utils/breadcrumbs.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from django.core.urlresolvers import resolve, get_script_prefix diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 7afe100a8..b6de18a81 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -1,13 +1,14 @@ """ Helper classes for parsers. """ +from __future__ import unicode_literals +from django.utils.datastructures import SortedDict +from rest_framework.compat import timezone +from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata import datetime import decimal import types import json -from django.utils.datastructures import SortedDict -from rest_framework.compat import timezone -from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata class JSONEncoder(json.JSONEncoder): diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py index aea1b6292..c09c29338 100644 --- a/rest_framework/utils/mediatypes.py +++ b/rest_framework/utils/mediatypes.py @@ -3,7 +3,7 @@ Handling of media types, as found in HTTP Content-Type and Accept headers. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 """ - +from __future__ import unicode_literals from django.http.multipartparser import parse_header from rest_framework import HTTP_HEADER_ENCODING diff --git a/rest_framework/views.py b/rest_framework/views.py index ac9b33855..ef2b5f920 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -1,8 +1,7 @@ """ Provides an APIView class that is used as the base of all class-based views. """ - -import re +from __future__ import unicode_literals from django.core.exceptions import PermissionDenied from django.http import Http404 from django.utils.html import escape @@ -13,6 +12,7 @@ from rest_framework.compat import View, apply_markdown from rest_framework.response import Response from rest_framework.request import Request from rest_framework.settings import api_settings +import re def _remove_trailing_string(content, trailing): From 0a38bc9db8c7ad5c1a9c8429ac799260c7257a39 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 4 Feb 2013 21:16:34 +0000 Subject: [PATCH 58/63] Deal with parser encodings properly --- rest_framework/parsers.py | 23 ++++++++++++++++++----- rest_framework/request.py | 2 ++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 26009cdf1..98d63fec1 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -5,6 +5,7 @@ They give us a generic way of being able to handle various media types on the request, such as form content or json encoded data. """ from __future__ import unicode_literals +from django.conf import settings from django.http import QueryDict from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError @@ -55,8 +56,11 @@ class JSONParser(BaseParser): `data` will be an object which is the parsed content of the response. `files` will always be `None`. """ + parser_context = parser_context or {} + encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) + try: - data = stream.read().decode('iso-8859-1') + data = stream.read().decode(encoding) return json.loads(data) except ValueError as exc: raise ParseError('JSON parse error - %s' % six.text_type(exc)) @@ -76,8 +80,11 @@ class YAMLParser(BaseParser): `data` will be an object which is the parsed content of the response. `files` will always be `None`. """ + parser_context = parser_context or {} + encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) + try: - data = stream.read().decode('iso-8859-1') + data = stream.read().decode(encoding) return yaml.safe_load(data) except (ValueError, yaml.parser.ParserError) as exc: raise ParseError('YAML parse error - %s' % six.u(exc)) @@ -97,7 +104,9 @@ class FormParser(BaseParser): `data` will be a :class:`QueryDict` containing all the form parameters. `files` will always be :const:`None`. """ - data = QueryDict(stream.read()) + parser_context = parser_context or {} + encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) + data = QueryDict(stream.read(), encoding=encoding) return data @@ -117,11 +126,12 @@ class MultiPartParser(BaseParser): """ parser_context = parser_context or {} request = parser_context['request'] + encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) meta = request.META upload_handlers = request.upload_handlers try: - parser = DjangoMultiPartParser(meta, stream, upload_handlers) + parser = DjangoMultiPartParser(meta, stream, upload_handlers, encoding) data, files = parser.parse() return DataAndFiles(data, files) except MultiPartParserError as exc: @@ -136,8 +146,11 @@ class XMLParser(BaseParser): media_type = 'application/xml' def parse(self, stream, media_type=None, parser_context=None): + parser_context = parser_context or {} + encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) + parser = ET.XMLParser(encoding=encoding) try: - tree = ET.parse(stream) + tree = ET.parse(stream, parser=parser) except (ExpatError, ETParseError, ValueError) as exc: raise ParseError('XML parse error - %s' % six.u(exc)) data = self._xml_convert(tree.getroot()) diff --git a/rest_framework/request.py b/rest_framework/request.py index 16f47d16b..482c86888 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -10,6 +10,7 @@ The wrapped request then offers a richer API, in particular : - form overloading of HTTP method, content type and content """ from __future__ import unicode_literals +from django.conf import settings from django.http.multipartparser import parse_header from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions @@ -92,6 +93,7 @@ class Request(object): if self.parser_context is None: self.parser_context = {} self.parser_context['request'] = self + self.parser_context['encoding'] = request.encoding or settings.DEFAULT_CHARSET def _default_negotiator(self): return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS() From 7dc4bce4e2d8bd21ba383ae1d62fdacf4998742e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 4 Feb 2013 21:35:47 +0000 Subject: [PATCH 59/63] Fix 2.6 compat --- rest_framework/compat.py | 9 +++++++++ rest_framework/parsers.py | 4 ++-- rest_framework/serializers.py | 5 +++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 8c64d9510..0d512342b 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -426,3 +426,12 @@ try: from xml.etree import ParseError as ETParseError except ImportError: # python < 2.7 ETParseError = None + + +# XMLParser only takes an encoding arg from >= 2.7 +def ET_XMLParser(encoding=None): + from xml.etree import ElementTree as ET + try: + return ET.XMLParser(encoding=encoding) + except TypeError: + return ET.XMLParser() diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 98d63fec1..06b022264 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -9,7 +9,7 @@ from django.conf import settings from django.http import QueryDict from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError -from rest_framework.compat import yaml, ETParseError +from rest_framework.compat import yaml, ETParseError, ET_XMLParser from rest_framework.exceptions import ParseError from rest_framework.compat import six from xml.etree import ElementTree as ET @@ -148,7 +148,7 @@ class XMLParser(BaseParser): def parse(self, stream, media_type=None, parser_context=None): parser_context = parser_context or {} encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) - parser = ET.XMLParser(encoding=encoding) + parser = ET_XMLParser(encoding=encoding) try: tree = ET.parse(stream, parser=parser) except (ExpatError, ETParseError, ValueError) as exc: diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b635d20d3..d9125e211 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -165,6 +165,11 @@ class BaseSerializer(Field): # Remove anything in 'exclude' if self.opts.exclude: + # Note: To be deprecated in line with Django's ModelForm change. + # https://code.djangoproject.com/ticket/19733 + warnings.warn('`exclude` option on serializers is due to be deprecated. ' + 'Use the `fields` option instead.', + PendingDeprecationWarning, stacklevel=2) for key in self.opts.exclude: ret.pop(key, None) From 280dfa4343fb7cf188f4573361bbec447b8ea7c2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 4 Feb 2013 21:45:38 +0000 Subject: [PATCH 60/63] Remove 2.0 notice on main docs. --- docs/index.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index 283ea069c..37e7cb3cf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,12 +11,6 @@ **A toolkit for building well-connected, self-describing Web APIs.** ---- - -**Note**: This documentation is for the 2.0 version of REST framework. If you are looking for earlier versions please see the [0.4.x branch][0.4] on GitHub. - ---- - Django REST framework is a lightweight library that makes it easy to build Web APIs. It is designed as a modular and easy to customize architecture, based on Django's class based views. Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box. From efb798cebcac63a0d60442d48c8a2d6102a58149 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 6 Feb 2013 08:48:41 +0000 Subject: [PATCH 61/63] Don't deprecate 'exclude' Need to track outcome of Django's #19733, and decide on approach accordingly. --- rest_framework/serializers.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d9125e211..b635d20d3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -165,11 +165,6 @@ class BaseSerializer(Field): # Remove anything in 'exclude' if self.opts.exclude: - # Note: To be deprecated in line with Django's ModelForm change. - # https://code.djangoproject.com/ticket/19733 - warnings.warn('`exclude` option on serializers is due to be deprecated. ' - 'Use the `fields` option instead.', - PendingDeprecationWarning, stacklevel=2) for key in self.opts.exclude: ret.pop(key, None) From 3a417733085914142bef861b7315ae4825f29ff8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 6 Feb 2013 08:48:54 +0000 Subject: [PATCH 62/63] Drop 2.0 notice. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 42c25c63e..62131ac68 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,6 @@ **Full documentation for REST framework is available on [http://django-rest-framework.org][docs].** -Note that this is the 2.0 version of REST framework. If you are looking for earlier versions please see the [0.4.x branch][0.4] on GitHub. - --- # Overview From bdc97c561147130b59c8cd8cc6bc735eab8b223d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 6 Feb 2013 08:52:21 +0000 Subject: [PATCH 63/63] 2.2 Release notes (wip) --- docs/topics/2.2-release-notes.md | 101 +++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/topics/2.2-release-notes.md diff --git a/docs/topics/2.2-release-notes.md b/docs/topics/2.2-release-notes.md new file mode 100644 index 000000000..32b3ad8c8 --- /dev/null +++ b/docs/topics/2.2-release-notes.md @@ -0,0 +1,101 @@ +# REST framework 2.2 release notes + +The 2.2 release represents an important point for REST framework, with the addition of Python 3 support, and the introduction of an official deprecation policy. + +## Python 3 support + +Thanks to some fantastic work from [Xavier Ordoquy][xordoquy], Django REST framework 2.2 now supports Python 3. You'll need to be running Django 1.5, and it's worth keeping in mind that Django's Python 3 support is currently [considered experimental][django-python-3]. + +Django 1.6's Python 3 support is expected to be officially labeled as 'production-ready'. + +If you want to start ensuring that your own projects are Python 3 ready, we can highly recommend Django's [Porting to Python 3][porting-python-3] documentation. + +## Deprecation policy + +We've now introduced an official deprecation policy, which is in line with [Django's deprecation policy][django-deprecation-policy]. This policy will make it easy for you to continue to track the latest, greatest version of REST framework. + +The timeline for deprecation works as follows: + +* Version 2.2 introduces some API changes as detailed in the release notes. It remains fully backwards compatible with 2.1, but will raise `PendingDeprecationWarning` warnings if you use bits API that are due to be deprecated. These warnings are silent by default, but can be explicitly enabled when you're ready to start migrating any required changes. For example if you start running your tests using `python -Wd manage.py test`, you'll be warned of any API changes you need to make. + +* Version 2.3 will escalate these warnings to `DeprecationWarning`, which is loud by default. + +* Version 2.4 will remove the deprecated bits of API entirely. + +Note that in line with Django's policy, any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change. + +## Community + +As of the 2.2 merge, we've also hit an impressive milestone. The number of committers listed in [the credits][credits], is now at over **one hundred individuals**. Each name on that list represents at least one merged pull request, however large or small. + +Our [mailing list][mailing-list] and #restframework IRC channel are also very active, and we've got a really impressive rate of development both on REST framework itself, and on third party packages such as the great [django-rest-framework-docs][django-rest-framework-docs] package from [Marc Gibbons][marcgibbons]. + +## API changes + +The 2.2 release makes a few changes to the serializer fields API, in order to make it more consistent, simple, and easier to use. + +### Cleaner to-many related fields + +The `ManyRelatedField()` style is being deprecated in favor of a new `RelatedField(many=True)` syntax. + +For example, if a user is associated with multiple questions, which we want to represent using a primary key relationship, we might use something like the following: + + class UserSerializer(serializers.HyperlinkedModelSerializer): + questions = serializers.PrimaryKeyRelatedField(many=True) + + class Meta: + fields = ('username', 'questions') + +The new syntax is cleaner and more obvious, and the change will also make the documentation cleaner, simplify the internal API, and make writing custom relational fields easier. + +The change also applies to serializers. If you have a nested serializer, you should start using `many=True` for to-many relationships. For example, a serializer representation of an Album that can contain many Tracks might look something like this: + + class TrackSerializer(serializer.ModelSerializer): + class Meta: + model = Track + fields = ('name', 'duration') + + class AlbumSerializer(serializer.ModelSerializer): + tracks = TrackSerializer(many=True) + + class Meta: + model = Album + fields = ('album_name', 'artist', 'tracks') + +Additionally, the change also applies when serializing or deserializing data. For example to serialize a queryset of models you should now use the `many=True` flag. + + serializer = SnippetSerializer(Snippet.objects.all(), many=True) + serializer.data + +This more explicit behavior on serializing and deserializing data [makes integration with non-ORM backends such as MongoDB easier][564], as instances to be serialized can include the `__iter__` method, without incorrectly triggering list-based serialization, or requiring workarounds. + +The implicit to-many behavior on serializers, and the `ManyRelatedField` style classes will continue to function, but will raise a `PendingDeprecationWarning`, which can be made visible using the `-Wd` flag. + +### Cleaner optional relationships + +Serializer relationships for nullable Foreign Keys will change from using the current `null=True` flag, to instead using `required=False`. + +This is in line both with the rest of the serializer fields API, and with Django's `Form` and `ModelForm` API. + +Using `required` throughout the serializers API means you won't need to consider if a particular field should take `blank` or `null` arguments instead of `required`, and also means there will be more consistent behavior for how fields are treated when they are not present in the incoming data. + +The `null=True` argument will continue to function, and will imply `required=False`, but will raise a `PendingDeprecationWarning`. + +### Cleaner CharField syntax + +The `CharField` API previously took an optional `blank=True` argument, which was intended to differentiate between null CharField input, and blank CharField input. + +In keeping with Django's CharField API, REST framework's `CharField` will only ever return the empty string, for missing or `None` inputs. The `blank` flag will no longer be in use, and you should instead just use the `required=` flag. + +The `blank` keyword argument will continue to function, but will raise a `PendingDeprecationWarning`. + +[xordoquy]: https://github.com/xordoquy +[django-python-3]: https://docs.djangoproject.com/en/dev/faq/install/#can-i-use-django-with-python-3 +[porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/ +[django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy +[credits]: http://django-rest-framework.org/topics/credits.html +[mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework +[django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs +[marcgibbons]: https://github.com/marcgibbons/ +[issues]: https://github.com/tomchristie/django-rest-framework/issues +[564]: https://github.com/tomchristie/django-rest-framework/issues/564