aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework/compat.py
diff options
context:
space:
mode:
Diffstat (limited to 'rest_framework/compat.py')
-rw-r--r--rest_framework/compat.py245
1 files changed, 220 insertions, 25 deletions
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 09b76368..581e29fc 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -2,21 +2,90 @@
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 django
+from django.core.exceptions import ImproperlyConfigured
+from django.conf import settings
+
+# Try to import six from Django, fallback to included `six`.
+try:
+ from django.utils import six
+except ImportError:
+ from rest_framework import six
+
+# location of patterns, url, include changes in 1.4 onwards
+try:
+ from django.conf.urls import patterns, url, include
+except ImportError:
+ 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
+
+
+# HttpResponseBase only exists from 1.5 onwards
+try:
+ from django.http.response import HttpResponseBase
+except ImportError:
+ from django.http import HttpResponse as HttpResponseBase
# django-filter is optional
try:
import django_filters
-except:
+except ImportError:
django_filters = None
+# guardian is optional
+try:
+ import guardian
+except ImportError:
+ guardian = None
+
# cStringIO only if it's available, otherwise StringIO
try:
- import cStringIO as StringIO
+ import cStringIO.StringIO as StringIO
+except ImportError:
+ StringIO = six.StringIO
+
+BytesIO = six.BytesIO
+
+
+# urlparse compat import (Required because it changed in python 3.x)
+try:
+ from urllib import parse as urlparse
except ImportError:
- import StringIO
+ import urlparse
+
+
+# Try to import PIL in either of the two ways it can end up installed.
+try:
+ from PIL import Image
+except ImportError:
+ try:
+ import Image
+ except ImportError:
+ Image = None
+
+
+def get_model_name(model_cls):
+ try:
+ return model_cls._meta.model_name
+ except AttributeError:
+ # < 1.6 used module_name instead of model_name
+ return model_cls._meta.module_name
def get_concrete_model(model_cls):
@@ -29,21 +98,12 @@ def get_concrete_model(model_cls):
# Django 1.5 add support for custom auth user model
if django.VERSION >= (1, 5):
- from django.conf import settings
- if hasattr(settings, 'AUTH_USER_MODEL'):
- User = settings.AUTH_USER_MODEL
- else:
- from django.contrib.auth.models import User
+ AUTH_USER_MODEL = settings.AUTH_USER_MODEL
else:
- try:
- from django.contrib.auth.models import User
- except ImportError:
- raise ImportError(u"User model is not to be found.")
+ AUTH_USER_MODEL = 'auth.User'
-# First implementation of Django class-based views did not include head method
-# in base View class - https://code.djangoproject.com/ticket/15668
-if django.VERSION >= (1, 4):
+if django.VERSION >= (1, 5):
from django.views.generic import View
else:
from django.views.generic import View as _View
@@ -51,6 +111,8 @@ else:
from django.utils.functional import update_wrapper
class View(_View):
+ # 1.3 does not include head method in base View class
+ # See: https://code.djangoproject.com/ticket/15668
@classonlymethod
def as_view(cls, **initkwargs):
"""
@@ -59,11 +121,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):
@@ -80,6 +142,16 @@ else:
update_wrapper(view, cls.dispatch, assigned=())
return view
+ # _allowed_methods only present from 1.5 onwards
+ def _allowed_methods(self):
+ return [m.upper() for m in self.http_method_names if hasattr(self, m)]
+
+
+# PATCH method is not implemented by Django
+if 'patch' not in View.http_method_names:
+ View.http_method_names = View.http_method_names + ['patch']
+
+
# PUT, DELETE do not require CSRF until 1.4. They should. Make it better.
if django.VERSION >= (1, 4):
from django.middleware.csrf import CsrfViewMiddleware
@@ -88,7 +160,6 @@ else:
import re
import random
import logging
- import urlparse
from django.conf import settings
from django.core.urlresolvers import get_callable
@@ -130,7 +201,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."
@@ -297,7 +369,7 @@ except ImportError:
# dateparse is ALSO new in Django 1.4
try:
- from django.utils.dateparse import parse_date, parse_datetime
+ from django.utils.dateparse import parse_date, parse_datetime, parse_time
except ImportError:
import datetime
import re
@@ -343,6 +415,77 @@ except ImportError:
kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
return datetime.datetime(**kw)
+
+# smart_urlquote is new on Django 1.4
+try:
+ from django.utils.html import smart_urlquote
+except ImportError:
+ import re
+ from django.utils.encoding import smart_str
+ try:
+ from urllib.parse import quote, urlsplit, urlunsplit
+ except ImportError: # Python 2
+ from urllib import quote
+ from urlparse import urlsplit, urlunsplit
+
+ unquoted_percents_re = re.compile(r'%(?![0-9A-Fa-f]{2})')
+
+ def smart_urlquote(url):
+ "Quotes a URL if it isn't already quoted."
+ # Handle IDN before quoting.
+ scheme, netloc, path, query, fragment = urlsplit(url)
+ try:
+ netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE
+ except UnicodeError: # invalid domain part
+ pass
+ else:
+ url = urlunsplit((scheme, netloc, path, query, fragment))
+
+ # An URL is considered unquoted if it contains no % characters or
+ # contains a % not followed by two hexadecimal digits. See #9655.
+ if '%' not in url or unquoted_percents_re.search(url):
+ # See http://bugs.python.org/issue2637
+ url = quote(smart_str(url), safe=b'!*\'();:@&=+$,/?#[]~')
+
+ return force_text(url)
+
+
+# RequestFactory only provide `generic` from 1.5 onwards
+
+from django.test.client import RequestFactory as DjangoRequestFactory
+from django.test.client import FakePayload
+try:
+ # In 1.5 the test client uses force_bytes
+ from django.utils.encoding import force_bytes_or_smart_bytes
+except ImportError:
+ # In 1.3 and 1.4 the test client just uses smart_str
+ from django.utils.encoding import smart_str as force_bytes_or_smart_bytes
+
+
+class RequestFactory(DjangoRequestFactory):
+ def generic(self, method, path,
+ data='', content_type='application/octet-stream', **extra):
+ parsed = urlparse.urlparse(path)
+ data = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET)
+ r = {
+ 'PATH_INFO': self._get_path(parsed),
+ 'QUERY_STRING': force_text(parsed[4]),
+ 'REQUEST_METHOD': str(method),
+ }
+ if data:
+ r.update({
+ 'CONTENT_LENGTH': len(data),
+ 'CONTENT_TYPE': str(content_type),
+ 'wsgi.input': FakePayload(data),
+ })
+ elif django.VERSION <= (1, 4):
+ # For 1.3 we need an empty WSGI payload
+ r.update({
+ 'wsgi.input': FakePayload('')
+ })
+ r.update(extra)
+ return self.request(**r)
+
# Markdown is optional
try:
import markdown
@@ -369,8 +512,60 @@ except ImportError:
yaml = None
-# xml.etree.parse only throws ParseError for python >= 2.7
+# XML is optional
+try:
+ import defusedxml.ElementTree as etree
+except ImportError:
+ etree = None
+
+# OAuth is optional
+try:
+ # Note: The `oauth2` package actually provides oauth1.0a support. Urg.
+ import oauth2 as oauth
+except ImportError:
+ oauth = None
+
+# OAuth is optional
+try:
+ import oauth_provider
+ from oauth_provider.store import store as oauth_provider_store
+except (ImportError, ImproperlyConfigured):
+ oauth_provider = None
+ oauth_provider_store = None
+
+# OAuth 2 support is optional
try:
- from xml.etree import ParseError as ETParseError
-except ImportError: # python < 2.7
- ETParseError = None
+ import provider.oauth2 as oauth2_provider
+ from provider.oauth2 import models as oauth2_provider_models
+ from provider.oauth2 import forms as oauth2_provider_forms
+ from provider import scope as oauth2_provider_scope
+ from provider import constants as oauth2_constants
+ from provider import __version__ as provider_version
+ if provider_version in ('0.2.3', '0.2.4'):
+ # 0.2.3 and 0.2.4 are supported version that do not support
+ # timezone aware datetimes
+ import datetime
+ provider_now = datetime.datetime.now
+ else:
+ # Any other supported version does use timezone aware datetimes
+ from django.utils.timezone import now as provider_now
+except ImportError:
+ oauth2_provider = None
+ oauth2_provider_models = None
+ oauth2_provider_forms = None
+ oauth2_provider_scope = None
+ oauth2_constants = None
+ provider_now = None
+
+# Handle lazy strings
+from django.utils.functional import Promise
+
+if six.PY3:
+ def is_non_str_iterable(obj):
+ if (isinstance(obj, str) or
+ (isinstance(obj, Promise) and obj._delegate_text)):
+ return False
+ return hasattr(obj, '__iter__')
+else:
+ def is_non_str_iterable(obj):
+ return hasattr(obj, '__iter__')