diff options
Diffstat (limited to 'rest_framework/compat.py')
| -rw-r--r-- | rest_framework/compat.py | 615 | 
1 files changed, 155 insertions, 460 deletions
| diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 6f7447ad..c6a4a869 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -5,34 +5,58 @@ 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`. +from django.utils.encoding import force_text +from django.utils.six.moves.urllib.parse import urlparse as _urlparse +from django.utils import six +import django +import inspect  try: -    from django.utils import six +    import importlib  except ImportError: -    from rest_framework import six +    from django.utils import importlib -# 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 +def unicode_repr(instance): +    # Get the repr of an instance, but ensure it is a unicode string +    # on both python 3 (already the case) and 2 (not the case). +    if six.PY2: +        return repr(instance).decode('utf-8') +    return repr(instance) -# 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 + +def unicode_to_repr(value): +    # Coerce a unicode string to the correct repr return type, depending on +    # the Python version. We wrap all our `__repr__` implementations with +    # this and then use unicode throughout internally. +    if six.PY2: +        return value.encode('utf-8') +    return value + + +def unicode_http_header(value): +    # Coerce HTTP header value to unicode. +    if isinstance(value, six.binary_type): +        return value.decode('iso-8859-1') +    return value + + +def total_seconds(timedelta): +    # TimeDelta.total_seconds() is only available in Python 2.7 +    if hasattr(timedelta, 'total_seconds'): +        return timedelta.total_seconds() +    else: +        return (timedelta.days * 86400.0) + float(timedelta.seconds) + (timedelta.microseconds / 1000000.0) + + +# OrderedDict only available in Python 2.7. +# This will always be the case in Django 1.7 and above, as these versions +# no longer support Python 2.6. +# For Django <= 1.6 and Python 2.6 fall back to SortedDict.  try: -    from django.utils.encoding import force_text +    from collections import OrderedDict  except ImportError: -    from django.utils.encoding import force_unicode as force_text +    from django.utils.datastructures import SortedDict as OrderedDict  # HttpResponseBase only exists from 1.5 onwards @@ -41,437 +65,163 @@ try:  except ImportError:      from django.http import HttpResponse as HttpResponseBase -# django-filter is optional -try: -    import django_filters -except ImportError: -    django_filters = None - -# cStringIO only if it's available, otherwise StringIO +# contrib.postgres only supported from 1.8 onwards.  try: -    import cStringIO.StringIO as StringIO +    from django.contrib.postgres import fields as postgres_fields  except ImportError: -    StringIO = six.StringIO - -BytesIO = six.BytesIO +    postgres_fields = None -# urlparse compat import (Required because it changed in python 3.x) -try: -    from urllib import parse as urlparse -except ImportError: -    import urlparse +# request only provides `resolver_match` from 1.5 onwards. +def get_resolver_match(request): +    try: +        return request.resolver_match +    except AttributeError: +        # Django < 1.5 +        from django.core.urlresolvers import resolve +        return resolve(request.path_info) -# Try to import PIL in either of the two ways it can end up installed. +# django-filter is optional  try: -    from PIL import Image +    import django_filters  except ImportError: +    django_filters = None + +if django.VERSION >= (1, 6): +    def clean_manytomany_helptext(text): +        return text +else: +    # Up to version 1.5 many to many fields automatically suffix +    # the `help_text` attribute with hardcoded text. +    def clean_manytomany_helptext(text): +        if text.endswith(' Hold down "Control", or "Command" on a Mac, to select more than one.'): +            text = text[:-69] +        return text + +# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS +# Fixes (#1712). We keep the try/except for the test suite. +guardian = None +if 'guardian' in settings.INSTALLED_APPS:      try: -        import Image +        import guardian +        import guardian.shortcuts  # Fixes #1624      except ImportError: -        Image = None +        pass -def get_concrete_model(model_cls): +def get_model_name(model_cls):      try: -        return model_cls._meta.concrete_model +        return model_cls._meta.model_name      except AttributeError: -        # 1.3 does not include concrete model -        return model_cls - - -# Django 1.5 add support for custom auth user model -if django.VERSION >= (1, 5): -    AUTH_USER_MODEL = settings.AUTH_USER_MODEL -else: -    AUTH_USER_MODEL = 'auth.User' +        # < 1.6 used module_name instead of model_name +        return model_cls._meta.module_name +# View._allowed_methods only present from 1.5 onwards  if django.VERSION >= (1, 5):      from django.views.generic import View  else: -    from django.views.generic import View as _View -    from django.utils.decorators import classonlymethod -    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): -            """ -            Main entry point for a request-response process. -            """ -            # sanitize keyword arguments -            for key in initkwargs: -                if key in cls.http_method_names: -                    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("%s() received an invalid keyword %r" % ( -                        cls.__name__, key)) - -            def view(request, *args, **kwargs): -                self = cls(**initkwargs) -                if hasattr(self, 'get') and not hasattr(self, 'head'): -                    self.head = self.get -                return self.dispatch(request, *args, **kwargs) - -            # take name and docstring from class -            update_wrapper(view, cls, updated=()) - -            # and possible attributes set by decorators -            # like csrf_exempt from dispatch -            update_wrapper(view, cls.dispatch, assigned=()) -            return view - -        # _allowed_methods only present from 1.5 onwards +    from django.views.generic import View as DjangoView + +    class View(DjangoView):          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 +# MinValueValidator, MaxValueValidator et al. only accept `message` in 1.8+ +if django.VERSION >= (1, 8): +    from django.core.validators import MinValueValidator, MaxValueValidator +    from django.core.validators import MinLengthValidator, MaxLengthValidator  else: -    import hashlib -    import re -    import random -    import logging - -    from django.conf import settings -    from django.core.urlresolvers import get_callable - -    try: -        from logging import NullHandler -    except ImportError: -        class NullHandler(logging.Handler): -            def emit(self, record): -                pass - -    logger = logging.getLogger('django.request') - -    if not logger.handlers: -        logger.addHandler(NullHandler()) - -    def same_origin(url1, url2): -        """ -        Checks if two URLs are 'same-origin' -        """ -        p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2) -        return p1[0:2] == p2[0:2] - -    def constant_time_compare(val1, val2): -        """ -        Returns True if the two strings are equal, False otherwise. - -        The time taken is independent of the number of characters that match. -        """ -        if len(val1) != len(val2): -            return False -        result = 0 -        for x, y in zip(val1, val2): -            result |= ord(x) ^ ord(y) -        return result == 0 - -    # Use the system (hardware-based) random number generator if it exists. -    if hasattr(random, 'SystemRandom'): -        randrange = random.SystemRandom().randrange -    else: -        randrange = random.randrange - -    _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." -    REASON_NO_CSRF_COOKIE = "CSRF cookie not set." -    REASON_BAD_TOKEN = "CSRF token missing or incorrect." - -    def _get_failure_view(): -        """ -        Returns the view to be used for CSRF rejections -        """ -        return get_callable(settings.CSRF_FAILURE_VIEW) - -    def _get_new_csrf_key(): -        return hashlib.md5("%s%s" % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest() - -    def get_token(request): -        """ -        Returns the the CSRF token required for a POST form. The token is an -        alphanumeric value. - -        A side effect of calling this function is to make the the csrf_protect -        decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie' -        header to the outgoing response.  For this reason, you may need to use this -        function lazily, as is done by the csrf context processor. -        """ -        request.META["CSRF_COOKIE_USED"] = True -        return request.META.get("CSRF_COOKIE", None) - -    def _sanitize_token(token): -        # Allow only alphanum, and ensure we return a 'str' for the sake of the post -        # processing middleware. -        token = re.sub('[^a-zA-Z0-9]', '', str(token.decode('ascii', 'ignore'))) -        if token == "": -            # In case the cookie has been truncated to nothing at some point. -            return _get_new_csrf_key() -        else: -            return token - -    class CsrfViewMiddleware(object): -        """ -        Middleware that requires a present and correct csrfmiddlewaretoken -        for POST requests that have a CSRF cookie, and sets an outgoing -        CSRF cookie. +    from django.core.validators import MinValueValidator as DjangoMinValueValidator +    from django.core.validators import MaxValueValidator as DjangoMaxValueValidator +    from django.core.validators import MinLengthValidator as DjangoMinLengthValidator +    from django.core.validators import MaxLengthValidator as DjangoMaxLengthValidator + +    class MinValueValidator(DjangoMinValueValidator): +        def __init__(self, *args, **kwargs): +            self.message = kwargs.pop('message', self.message) +            super(MinValueValidator, self).__init__(*args, **kwargs) + +    class MaxValueValidator(DjangoMaxValueValidator): +        def __init__(self, *args, **kwargs): +            self.message = kwargs.pop('message', self.message) +            super(MaxValueValidator, self).__init__(*args, **kwargs) + +    class MinLengthValidator(DjangoMinLengthValidator): +        def __init__(self, *args, **kwargs): +            self.message = kwargs.pop('message', self.message) +            super(MinLengthValidator, self).__init__(*args, **kwargs) + +    class MaxLengthValidator(DjangoMaxLengthValidator): +        def __init__(self, *args, **kwargs): +            self.message = kwargs.pop('message', self.message) +            super(MaxLengthValidator, self).__init__(*args, **kwargs) + + +# URLValidator only accepts `message` in 1.6+ +if django.VERSION >= (1, 6): +    from django.core.validators import URLValidator +else: +    from django.core.validators import URLValidator as DjangoURLValidator -        This middleware should be used in conjunction with the csrf_token template -        tag. -        """ -        # The _accept and _reject methods currently only exist for the sake of the -        # requires_csrf_token decorator. -        def _accept(self, request): -            # Avoid checking the request twice by adding a custom attribute to -            # request.  This will be relevant when both decorator and middleware -            # are used. -            request.csrf_processing_done = True -            return None - -        def _reject(self, request, reason): -            return _get_failure_view()(request, reason=reason) - -        def process_view(self, request, callback, callback_args, callback_kwargs): - -            if getattr(request, 'csrf_processing_done', False): -                return None - -            try: -                csrf_token = _sanitize_token(request.COOKIES[settings.CSRF_COOKIE_NAME]) -                # Use same token next time -                request.META['CSRF_COOKIE'] = csrf_token -            except KeyError: -                csrf_token = None -                # Generate token and store it in the request, so it's available to the view. -                request.META["CSRF_COOKIE"] = _get_new_csrf_key() - -            # Wait until request.META["CSRF_COOKIE"] has been manipulated before -            # bailing out, so that get_token still works -            if getattr(callback, 'csrf_exempt', False): -                return None - -            # Assume that anything not defined as 'safe' by RC2616 needs protection. -            if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'): -                if getattr(request, '_dont_enforce_csrf_checks', False): -                    # Mechanism to turn off CSRF checks for test suite.  It comes after -                    # the creation of CSRF cookies, so that everything else continues to -                    # work exactly the same (e.g. cookies are sent etc), but before the -                    # any branches that call reject() -                    return self._accept(request) - -                if request.is_secure(): -                    # Suppose user visits http://example.com/ -                    # An active network attacker,(man-in-the-middle, MITM) sends a -                    # POST form which targets https://example.com/detonate-bomb/ and -                    # submits it via javascript. -                    # -                    # The attacker will need to provide a CSRF cookie and token, but -                    # that is no problem for a MITM and the session independent -                    # nonce we are using. So the MITM can circumvent the CSRF -                    # protection. This is true for any HTTP connection, but anyone -                    # using HTTPS expects better!  For this reason, for -                    # https://example.com/ we need additional protection that treats -                    # http://example.com/ as completely untrusted.  Under HTTPS, -                    # Barth et al. found that the Referer header is missing for -                    # same-domain requests in only about 0.2% of cases or less, so -                    # we can use strict Referer checking. -                    referer = request.META.get('HTTP_REFERER') -                    if referer is None: -                        logger.warning('Forbidden (%s): %s' % (REASON_NO_REFERER, request.path), -                            extra={ -                                'status_code': 403, -                                'request': request, -                            } -                        ) -                        return self._reject(request, REASON_NO_REFERER) - -                    # Note that request.get_host() includes the port -                    good_referer = 'https://%s/' % request.get_host() -                    if not same_origin(referer, good_referer): -                        reason = REASON_BAD_REFERER % (referer, good_referer) -                        logger.warning('Forbidden (%s): %s' % (reason, request.path), -                            extra={ -                                'status_code': 403, -                                'request': request, -                            } -                        ) -                        return self._reject(request, reason) - -                if csrf_token is None: -                    # No CSRF cookie. For POST requests, we insist on a CSRF cookie, -                    # and in this way we can avoid all CSRF attacks, including login -                    # CSRF. -                    logger.warning('Forbidden (%s): %s' % (REASON_NO_CSRF_COOKIE, request.path), -                        extra={ -                            'status_code': 403, -                            'request': request, -                        } -                    ) -                    return self._reject(request, REASON_NO_CSRF_COOKIE) - -                # check non-cookie token for match -                request_csrf_token = "" -                if request.method == "POST": -                    request_csrf_token = request.POST.get('csrfmiddlewaretoken', '') - -                if request_csrf_token == "": -                    # Fall back to X-CSRFToken, to make things easier for AJAX, -                    # and possible for PUT/DELETE -                    request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '') - -                if not constant_time_compare(request_csrf_token, csrf_token): -                    logger.warning('Forbidden (%s): %s' % (REASON_BAD_TOKEN, request.path), -                        extra={ -                            'status_code': 403, -                            'request': request, -                        } -                    ) -                    return self._reject(request, REASON_BAD_TOKEN) - -            return self._accept(request) - -# timezone support is new in Django 1.4 -try: -    from django.utils import timezone -except ImportError: -    timezone = None +    class URLValidator(DjangoURLValidator): +        def __init__(self, *args, **kwargs): +            self.message = kwargs.pop('message', self.message) +            super(URLValidator, self).__init__(*args, **kwargs) -# dateparse is ALSO new in Django 1.4 -try: -    from django.utils.dateparse import parse_date, parse_datetime, parse_time -except ImportError: -    import datetime -    import re - -    date_re = re.compile( -        r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$' -    ) - -    datetime_re = re.compile( -        r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})' -        r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})' -        r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?' -        r'(?P<tzinfo>Z|[+-]\d{1,2}:\d{1,2})?$' -    ) - -    time_re = re.compile( -        r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})' -        r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?' -    ) - -    def parse_date(value): -        match = date_re.match(value) -        if match: -            kw = dict((k, int(v)) for k, v in match.groupdict().iteritems()) -            return datetime.date(**kw) - -    def parse_time(value): -        match = time_re.match(value) -        if match: -            kw = match.groupdict() -            if kw['microsecond']: -                kw['microsecond'] = kw['microsecond'].ljust(6, '0') -            kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None) -            return datetime.time(**kw) - -    def parse_datetime(value): -        """Parse datetime, but w/o the timezone awareness in 1.4""" -        match = datetime_re.match(value) -        if match: -            kw = match.groupdict() -            if kw['microsecond']: -                kw['microsecond'] = kw['microsecond'].ljust(6, '0') -            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)) +# EmailValidator requires explicit regex prior to 1.6+ +if django.VERSION >= (1, 6): +    from django.core.validators import EmailValidator +else: +    from django.core.validators import EmailValidator as DjangoEmailValidator +    from django.core.validators import email_re -        # 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'!*\'();:@&=+$,/?#[]~') +    class EmailValidator(DjangoEmailValidator): +        def __init__(self, *args, **kwargs): +            super(EmailValidator, self).__init__(email_re, *args, **kwargs) -        return force_text(url) +# PATCH method is not implemented by Django +if 'patch' not in View.http_method_names: +    View.http_method_names = View.http_method_names + ['patch'] -# RequestFactory only provide `generic` from 1.5 onwards +# RequestFactory only provides `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 +    from django.utils.encoding import force_bytes as force_bytes_or_smart_bytes  except ImportError: -    # In 1.3 and 1.4 the test client just uses smart_str +    # In 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) +        parsed = _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), +            'PATH_INFO': self._get_path(parsed), +            'QUERY_STRING': force_text(parsed[4]), +            'REQUEST_METHOD': six.text_type(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('') +                'CONTENT_TYPE': six.text_type(content_type), +                'wsgi.input': FakePayload(data),              })          r.update(extra)          return self.request(**r) +  # Markdown is optional  try:      import markdown @@ -486,72 +236,17 @@ try:          safe_mode = False          md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode)          return md.convert(text) -  except ImportError:      apply_markdown = None -# Yaml is optional -try: -    import yaml -except ImportError: -    yaml = None - - -# 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: -    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 - +# `separators` argument to `json.dumps()` differs between 2.x and 3.x +# See: http://bugs.python.org/issue22767  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__') +    SHORT_SEPARATORS = (',', ':') +    LONG_SEPARATORS = (', ', ': ') +    INDENT_SEPARATORS = (',', ': ')  else: -    def is_non_str_iterable(obj): -        return hasattr(obj, '__iter__') +    SHORT_SEPARATORS = (b',', b':') +    LONG_SEPARATORS = (b', ', b': ') +    INDENT_SEPARATORS = (b',', b': ') | 
