diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index b9e3f9817..17616c057 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -7,7 +7,6 @@ ______ _____ _____ _____ __ \_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_| """ -import django __title__ = 'Django REST framework' __version__ = '3.14.0' @@ -25,10 +24,6 @@ HTTP_HEADER_ENCODING = 'iso-8859-1' ISO_8601 = 'iso-8601' -if django.VERSION < (3, 2): - default_app_config = 'rest_framework.apps.RestFrameworkConfig' - - class RemovedInDRF315Warning(DeprecationWarning): pass diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 3f3bd2227..58ef9d2e1 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -17,7 +17,7 @@ def get_authorization_header(request): Hide some test client ickyness where the header can be unicode. """ - auth = request.META.get('HTTP_AUTHORIZATION', b'') + auth = request.headers.get('authorization', b'') if isinstance(auth, str): # Work around django test client oddness auth = auth.encode(HTTP_HEADER_ENCODING) diff --git a/rest_framework/authtoken/__init__.py b/rest_framework/authtoken/__init__.py index 285fe15c6..e69de29bb 100644 --- a/rest_framework/authtoken/__init__.py +++ b/rest_framework/authtoken/__init__.py @@ -1,4 +0,0 @@ -import django - -if django.VERSION < (3, 2): - default_app_config = 'rest_framework.authtoken.apps.AuthTokenConfig' diff --git a/rest_framework/authtoken/admin.py b/rest_framework/authtoken/admin.py index 163328eb0..cb89f127d 100644 --- a/rest_framework/authtoken/admin.py +++ b/rest_framework/authtoken/admin.py @@ -21,6 +21,7 @@ class TokenChangeList(ChangeList): current_app=self.model_admin.admin_site.name) +@admin.register(TokenProxy) class TokenAdmin(admin.ModelAdmin): list_display = ('key', 'user', 'created') fields = ('user',) @@ -50,6 +51,3 @@ class TokenAdmin(admin.ModelAdmin): # Map back to actual Token, since delete() uses pk. token = Token.objects.get(key=obj.key) return super().delete_model(request, token) - - -admin.site.register(TokenProxy, TokenAdmin) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 472b8ad24..f9e27e20f 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -145,29 +145,10 @@ else: return False -if django.VERSION >= (4, 2): - # Django 4.2+: use the stock parse_header_parameters function - # Note: Django 4.1 also has an implementation of parse_header_parameters - # which is slightly different from the one in 4.2, it needs - # the compatibility shim as well. - from django.utils.http import parse_header_parameters -else: - # Django <= 4.1: create a compatibility shim for parse_header_parameters - from django.http.multipartparser import parse_header - - def parse_header_parameters(line): - # parse_header works with bytes, but parse_header_parameters - # works with strings. Call encode to convert the line to bytes. - main_value_pair, params = parse_header(line.encode()) - return main_value_pair, { - # parse_header will convert *some* values to string. - # parse_header_parameters converts *all* values to string. - # Make sure all values are converted by calling decode on - # any remaining non-string values. - k: v if isinstance(v, str) else v.decode() - for k, v in params.items() - } - +# Django 4.2+: use the stock parse_header_parameters function +# Note: Django 4.1 also has an implementation of parse_header_parameters +# which is slightly different from the one in 4.2, it needs +# the compatibility shim as well. if django.VERSION >= (5, 1): # Django 5.1+: use the stock ip_address_validators function diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index b4bbfa1f5..da6922bdb 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -93,5 +93,5 @@ class DefaultContentNegotiation(BaseContentNegotiation): Given the incoming request, return a tokenized list of media type strings. """ - header = request.META.get('HTTP_ACCEPT', '*/*') + header = request.headers.get('accept', '*/*') return [token.strip() for token in header.split(',')] diff --git a/rest_framework/test.py b/rest_framework/test.py index 04409f962..e939adcd7 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -3,7 +3,6 @@ import io from importlib import import_module -import django from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.handlers.wsgi import WSGIHandler @@ -394,19 +393,7 @@ class URLPatternsTestCase(testcases.SimpleTestCase): cls._override.enable() - if django.VERSION > (4, 0): - cls.addClassCleanup(cls._override.disable) - cls.addClassCleanup(cleanup_url_patterns, cls) + cls.addClassCleanup(cls._override.disable) + cls.addClassCleanup(cleanup_url_patterns, cls) super().setUpClass() - - if django.VERSION < (4, 0): - @classmethod - def tearDownClass(cls): - super().tearDownClass() - cls._override.disable() - - if hasattr(cls, '_module_urlpatterns'): - cls._module.urlpatterns = cls._module_urlpatterns - else: - del cls._module.urlpatterns diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index c0d6cf42f..90ce01c42 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -26,7 +26,7 @@ class BaseThrottle: if present and number of proxies is > 0. If not use all of HTTP_X_FORWARDED_FOR if it is available, if not use REMOTE_ADDR. """ - xff = request.META.get('HTTP_X_FORWARDED_FOR') + xff = request.headers.get('x-forwarded-for') remote_addr = request.META.get('REMOTE_ADDR') num_proxies = api_settings.NUM_PROXIES diff --git a/tests/authentication/test_authentication.py b/tests/authentication/test_authentication.py index 22e837ef4..b07fae8d6 100644 --- a/tests/authentication/test_authentication.py +++ b/tests/authentication/test_authentication.py @@ -1,6 +1,5 @@ import base64 -import django import pytest from django.conf import settings from django.contrib.auth.models import User @@ -236,15 +235,12 @@ class SessionAuthTests(TestCase): Regression test for #6088 """ # Remove this shim when dropping support for Django 3.0. - if django.VERSION < (3, 1): - from django.middleware.csrf import _get_new_csrf_token - else: - from django.middleware.csrf import ( - _get_new_csrf_string, _mask_cipher_secret - ) + from django.middleware.csrf import ( + _get_new_csrf_string, _mask_cipher_secret + ) - def _get_new_csrf_token(): - return _mask_cipher_secret(_get_new_csrf_string()) + def _get_new_csrf_token(): + return _mask_cipher_secret(_get_new_csrf_string()) self.csrf_client.login(username=self.username, password=self.password) diff --git a/tests/conftest.py b/tests/conftest.py index b67475d8a..0ae85a99c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -87,10 +87,7 @@ def pytest_configure(config): import rest_framework settings.STATIC_ROOT = os.path.join(os.path.dirname(rest_framework.__file__), 'static-root') backend = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' - if django.VERSION < (4, 2): - settings.STATICFILES_STORAGE = backend - else: - settings.STORAGES['staticfiles']['BACKEND'] = backend + settings.STORAGES['staticfiles']['BACKEND'] = backend django.setup() diff --git a/tests/test_api_client.py b/tests/test_api_client.py index 976f10ed1..b2d322de2 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -4,7 +4,7 @@ import unittest from django.http import HttpResponse from django.test import override_settings -from django.urls import path, re_path +from django.urls import path from rest_framework.compat import coreapi, coreschema from rest_framework.parsers import FileUploadParser @@ -180,7 +180,7 @@ class HeadersView(APIView): urlpatterns = [ path('', SchemaView.as_view()), path('example/', ListView.as_view()), - re_path(r'^example/(?P[0-9]+)/$', DetailView.as_view()), + path('example//', DetailView.as_view()), path('upload/', UploadView.as_view()), path('download/', DownloadView.as_view()), path('text/', TextView.as_view()), diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 6b2c91db7..42ff7aea2 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -65,7 +65,7 @@ class TestMiddleware(APITestCase): key = 'abcd1234' Token.objects.create(key=key, user=user) - self.client.get('/auth', HTTP_AUTHORIZATION='Token %s' % key) + self.client.get('/auth', headers={"authorization": 'Token %s' % key}) @override_settings(MIDDLEWARE=('tests.test_middleware.RequestPOSTMiddleware',)) def test_middleware_can_access_request_post_when_processing_response(self): diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 20d0319fc..b8cce326a 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -11,7 +11,6 @@ import json # noqa import sys import tempfile -import django import pytest from django.core.exceptions import ImproperlyConfigured from django.core.serializers.json import DjangoJSONEncoder @@ -454,8 +453,6 @@ class TestPosgresFieldsMapping(TestCase): fields = ['array_field', 'array_field_with_blank'] validators = "" - if django.VERSION < (4, 1): - validators = ", validators=[]" expected = dedent(""" TestSerializer(): array_field = ListField(allow_empty=False, child=CharField(label='Array field'%s)) diff --git a/tests/test_relations.py b/tests/test_relations.py index b9ab15789..28f8e9d66 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -4,7 +4,7 @@ import pytest from _pytest.monkeypatch import MonkeyPatch from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.test import override_settings -from django.urls import re_path +from django.urls import path from django.utils.datastructures import MultiValueDict from rest_framework import relations, serializers @@ -152,7 +152,7 @@ class TestProxiedPrimaryKeyRelatedField(APISimpleTestCase): urlpatterns = [ - re_path(r'^example/(?P.+)/$', lambda: None, name='example'), + path('example//', lambda: None, name='example'), ] diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 247737576..462c5b4da 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -174,7 +174,7 @@ class RendererEndToEndTests(TestCase): def test_default_renderer_serializes_content_on_accept_any(self): """If the Accept header is set to */* the default renderer should serialize the response.""" - resp = self.client.get('/', HTTP_ACCEPT='*/*') + resp = self.client.get('/', headers={"accept": '*/*'}) self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8') self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) @@ -182,7 +182,7 @@ class RendererEndToEndTests(TestCase): def test_specified_renderer_serializes_content_default_case(self): """If the Accept header is set the specified renderer should serialize the response. (In this case we check that works for the default renderer)""" - resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type) + resp = self.client.get('/', headers={"accept": RendererA.media_type}) self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8') self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) @@ -190,14 +190,14 @@ class RendererEndToEndTests(TestCase): def test_specified_renderer_serializes_content_non_default_case(self): """If the Accept header is set the specified renderer should serialize the response. (In this case we check that works for a non-default renderer)""" - resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type) + resp = self.client.get('/', headers={"accept": RendererB.media_type}) self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8') self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) def test_unsatisfiable_accept_header_on_request_returns_406_status(self): """If the Accept header is unsatisfiable we should return a 406 Not Acceptable response.""" - resp = self.client.get('/', HTTP_ACCEPT='foo/bar') + resp = self.client.get('/', headers={"accept": 'foo/bar'}) self.assertEqual(resp.status_code, status.HTTP_406_NOT_ACCEPTABLE) def test_specified_renderer_serializes_content_on_format_query(self): @@ -228,14 +228,14 @@ class RendererEndToEndTests(TestCase): RendererB.format ) resp = self.client.get('/' + param, - HTTP_ACCEPT=RendererB.media_type) + headers={"accept": RendererB.media_type}) self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8') self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) def test_parse_error_renderers_browsable_api(self): """Invalid data should still render the browsable API correctly.""" - resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html') + resp = self.client.post('/parseerror', data='foobar', content_type='application/json', headers={"accept": 'text/html'}) self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) @@ -714,13 +714,13 @@ class BrowsableAPIRendererTests(URLPatternsTestCase): assert result is None def test_extra_actions_dropdown(self): - resp = self.client.get('/api/examples/', HTTP_ACCEPT='text/html') + resp = self.client.get('/api/examples/', headers={"accept": 'text/html'}) assert 'id="extra-actions-menu"' in resp.content.decode() assert '/api/examples/list_action/' in resp.content.decode() assert '>Extra list action<' in resp.content.decode() def test_extra_actions_dropdown_not_authed(self): - resp = self.client.get('/api/unauth-examples/', HTTP_ACCEPT='text/html') + resp = self.client.get('/api/unauth-examples/', headers={"accept": 'text/html'}) assert 'id="extra-actions-menu"' not in resp.content.decode() assert '/api/examples/list_action/' not in resp.content.decode() assert '>Extra list action<' not in resp.content.decode() diff --git a/tests/test_requests_client.py b/tests/test_requests_client.py index c8e7be6ee..9b4f4795f 100644 --- a/tests/test_requests_client.py +++ b/tests/test_requests_client.py @@ -28,7 +28,7 @@ class Root(APIView): } post = request.POST json = None - if request.META.get('CONTENT_TYPE') == 'application/json': + if request.headers.get('content-type') == 'application/json': json = request.data return Response({ diff --git a/tests/test_response.py b/tests/test_response.py index cab19a1eb..82bac3d4a 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -154,7 +154,7 @@ class RendererIntegrationTests(TestCase): def test_default_renderer_serializes_content_on_accept_any(self): """If the Accept header is set to */* the default renderer should serialize the response.""" - resp = self.client.get('/', HTTP_ACCEPT='*/*') + resp = self.client.get('/', headers={"accept": '*/*'}) self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8') self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) @@ -162,7 +162,7 @@ class RendererIntegrationTests(TestCase): def test_specified_renderer_serializes_content_default_case(self): """If the Accept header is set the specified renderer should serialize the response. (In this case we check that works for the default renderer)""" - resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type) + resp = self.client.get('/', headers={"accept": RendererA.media_type}) self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8') self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) @@ -170,7 +170,7 @@ class RendererIntegrationTests(TestCase): def test_specified_renderer_serializes_content_non_default_case(self): """If the Accept header is set the specified renderer should serialize the response. (In this case we check that works for a non-default renderer)""" - resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type) + resp = self.client.get('/', headers={"accept": RendererB.media_type}) self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8') self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) @@ -195,7 +195,7 @@ class RendererIntegrationTests(TestCase): """If both a 'format' query and a matching Accept header specified, the renderer with the matching format attribute should serialize the response.""" resp = self.client.get('/?format=%s' % RendererB.format, - HTTP_ACCEPT=RendererB.media_type) + headers={"accept": RendererB.media_type}) self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8') self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) diff --git a/tests/test_testing.py b/tests/test_testing.py index 196319a29..f3b7336de 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -2,7 +2,6 @@ import itertools from io import BytesIO from unittest.mock import patch -import django from django.contrib.auth.models import User from django.http import HttpResponseRedirect from django.shortcuts import redirect @@ -20,7 +19,7 @@ from rest_framework.test import ( @api_view(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) def view(request): - data = {'auth': request.META.get('HTTP_AUTHORIZATION', b'')} + data = {'auth': request.headers.get('authorization', b'')} if request.user: data['user'] = request.user.username if request.auth: @@ -316,7 +315,7 @@ class TestAPIRequestFactory(TestCase): data=None, content_type='application/json', ) - assert request.META['CONTENT_TYPE'] == 'application/json' + assert request.headers['content-type'] == 'application/json' def check_urlpatterns(cls): @@ -334,18 +333,10 @@ class TestUrlPatternTestCase(URLPatternsTestCase): super().setUpClass() assert urlpatterns is cls.urlpatterns - if django.VERSION > (4, 0): - cls.addClassCleanup( - check_urlpatterns, - cls - ) - - if django.VERSION < (4, 0): - @classmethod - def tearDownClass(cls): - assert urlpatterns is cls.urlpatterns - super().tearDownClass() - assert urlpatterns is not cls.urlpatterns + cls.addClassCleanup( + check_urlpatterns, + cls + ) def test_urlpatterns(self): assert self.client.get('/').status_code == 200 diff --git a/tests/test_urlpatterns.py b/tests/test_urlpatterns.py index adcd0a742..db7092ff7 100644 --- a/tests/test_urlpatterns.py +++ b/tests/test_urlpatterns.py @@ -1,7 +1,7 @@ from collections import namedtuple from django.test import TestCase -from django.urls import Resolver404, URLResolver, include, path, re_path +from django.urls import Resolver404, URLResolver, include, path from django.urls.resolvers import RegexPattern from rest_framework.test import APIRequestFactory @@ -93,7 +93,7 @@ class FormatSuffixTests(TestCase): def test_format_suffix_django2_args(self): urlpatterns = [ path('convtest/', dummy_view), - re_path(r'^retest/(?P[0-9]+)$', dummy_view), + path('retest/', dummy_view), ] test_paths = [ URLTestPath('/convtest/42', (), {'pk': 42}), @@ -145,10 +145,10 @@ class FormatSuffixTests(TestCase): def test_included_urls_mixed(self): nested_patterns = [ path('path/', dummy_view), - re_path(r'^re_path/(?P[0-9]+)$', dummy_view) + path('re_path/', dummy_view) ] urlpatterns = [ - re_path(r'^pre_path/(?P[0-9]+)/', include(nested_patterns), {'foo': 'bar'}), + path('pre_path//', include(nested_patterns), {'foo': 'bar'}), path('ppath//', include(nested_patterns), {'foo': 'bar'}), ] test_paths = [ @@ -185,7 +185,7 @@ class FormatSuffixTests(TestCase): def test_allowed_formats_re_path(self): urlpatterns = [ - re_path(r'^test$', dummy_view), + path('test', dummy_view), ] self._test_allowed_formats(urlpatterns) diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 1ccecae0b..1b0946f69 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -152,7 +152,7 @@ class TestURLReversing(URLPatternsTestCase, APITestCase): path('v1/', include((included, 'v1'), namespace='v1')), path('another/', dummy_view, name='another'), re_path(r'^(?P[v1|v2]+)/another/$', dummy_view, name='another'), - re_path(r'^(?P.+)/unversioned/$', dummy_view, name='unversioned'), + path('/unversioned/', dummy_view, name='unversioned'), ] diff --git a/tox.ini b/tox.ini index 2b8733d7d..62e897494 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,13 @@ [tox] +; https://docs.djangoproject.com/en/stable/faq/install/#what-python-version-can-i-use-with-django envlist = {py36,py37,py38,py39}-django30 {py36,py37,py38,py39}-django31 - {py36,py37,py38,py39,py310}-django32 - {py38,py39,py310}-{django40,django41,django42,djangomain} - {py311}-{django41,django42,djangomain} + {py36,py37,py38,py39}-django32 + {py38,py39}-{django40,django41,django42} + {py310}-{django32,django42,django50,djangomain} + {py311}-{django42,django50,djangomain} + {py312}-{django42,django50,djangomain} base dist docs @@ -22,6 +25,7 @@ deps = django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 django42: Django>=4.2,<5.0 + django50: Django>=5.0,<5.1 djangomain: https://github.com/django/django/archive/main.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt