diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 274994c20b..779dcf67eb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,3 +43,7 @@ repos: rev: v2.11.1 hooks: - id: pyproject-fmt +- repo: https://github.com/adamchainz/django-upgrade + rev: 1.24.0 + hooks: + - id: django-upgrade diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 3f3bd2227c..58ef9d2e10 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/admin.py b/rest_framework/authtoken/admin.py index 80d8d445fb..a48b190c8c 100644 --- a/rest_framework/authtoken/admin.py +++ b/rest_framework/authtoken/admin.py @@ -21,6 +21,7 @@ def url_for_result(self, result): current_app=self.model_admin.admin_site.name) +@admin.register(TokenProxy) class TokenAdmin(admin.ModelAdmin): list_display = ('key', 'user', 'created') list_filter = ('created',) @@ -50,6 +51,3 @@ def delete_model(self, request, obj): # 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/negotiation.py b/rest_framework/negotiation.py index 23012f71fd..d0b43ccd85 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -93,5 +93,5 @@ def get_accept_list(self, request): 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/throttling.py b/rest_framework/throttling.py index c0d6cf42fe..90ce01c428 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -26,7 +26,7 @@ def get_ident(self, request): 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/test_decorators.py b/tests/test_decorators.py index cc7cab4d7d..6fdac9b10e 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -178,7 +178,7 @@ def view(request): def test_content_negotiation(self): class CustomContentNegotiation(BaseContentNegotiation): def select_renderer(self, request, renderers, format_suffix): - assert request.META['HTTP_ACCEPT'] == 'custom/type' + assert request.headers['accept'] == 'custom/type' return (renderers[0], renderers[0].media_type) @api_view(["GET"]) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index ddf0825968..fcc0748196 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -99,7 +99,7 @@ def test_middleware_can_access_user_when_processing_response(self): 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_relations.py b/tests/test_relations.py index b9ab157896..28f8e9d668 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -4,7 +4,7 @@ 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 @@ def test_pk_representation(self): 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 3d5f6d758f..943893b06d 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -174,7 +174,7 @@ def test_head_method_serializes_no_content(self): 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 @@ def test_default_renderer_serializes_content_on_accept_any(self): 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 @@ def test_specified_renderer_serializes_content_default_case(self): 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 @@ def test_specified_renderer_is_used_on_format_query_with_matching_accept(self): 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) @@ -834,19 +834,19 @@ class DummyView: 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() def test_options_form_does_not_check_object_permissions_for_extra_action(self): - resp = self.client.get('/api/issue-6855/1/extra_action/', HTTP_ACCEPT='text/html') + resp = self.client.get('/api/issue-6855/1/extra_action/', headers={"accept": 'text/html'}) assert resp.status_code == status.HTTP_200_OK def test_delete_form_still_checks_object_permissions(self): diff --git a/tests/test_requests_client.py b/tests/test_requests_client.py index c8e7be6ee3..9b4f4795fb 100644 --- a/tests/test_requests_client.py +++ b/tests/test_requests_client.py @@ -28,7 +28,7 @@ def post(self, request): } 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 83f8a67172..781478b7a3 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -151,7 +151,7 @@ def test_head_method_serializes_no_content(self): 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) @@ -159,7 +159,7 @@ def test_default_renderer_serializes_content_on_accept_any(self): 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) @@ -167,7 +167,7 @@ def test_specified_renderer_serializes_content_default_case(self): 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) @@ -192,7 +192,7 @@ def test_specified_renderer_is_used_on_format_query_with_matching_accept(self): """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 fed42342a3..5dffb0d130 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -22,7 +22,7 @@ @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: @@ -370,7 +370,7 @@ def test_empty_request_content_type(self): data=None, content_type='application/json', ) - assert request.META['CONTENT_TYPE'] == 'application/json' + assert request.headers['content-type'] == 'application/json' class TestUrlPatternTestCase(URLPatternsTestCase): diff --git a/tests/test_throttling.py b/tests/test_throttling.py index be9decebc5..d3341f93bb 100644 --- a/tests/test_throttling.py +++ b/tests/test_throttling.py @@ -392,9 +392,16 @@ def get(self, request): cache.clear() self.throttle = Throttle() self.view = View.as_view() - self.request = APIRequestFactory().get('/some_uri') - self.request.META['REMOTE_ADDR'] = '3.3.3.3' - self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 1.1.1.1, 2.2.2.2' + self.request = self.build_request('0.0.0.0, 1.1.1.1, 2.2.2.2') + + def build_request(self, xff): + # `HttpRequest.headers` is a cached_property, so a request has to be + # rebuilt rather than have its META mutated to change a header. + request = APIRequestFactory().get( + '/some_uri', headers={'x-forwarded-for': xff} + ) + request.META['REMOTE_ADDR'] = '3.3.3.3' + return request def config_proxy(self, num_proxies): setattr(api_settings, 'NUM_PROXIES', num_proxies) @@ -415,28 +422,28 @@ class XffSpoofingTests(XffTestingBase): def test_xff_spoofing_doesnt_change_machine_id_with_one_app_proxy(self): self.config_proxy(1) self.view(self.request) - self.request.META['HTTP_X_FORWARDED_FOR'] = '4.4.4.4, 5.5.5.5, 2.2.2.2' - assert self.view(self.request).status_code == 429 + spoofed = self.build_request('4.4.4.4, 5.5.5.5, 2.2.2.2') + assert self.view(spoofed).status_code == 429 def test_xff_spoofing_doesnt_change_machine_id_with_two_app_proxies(self): self.config_proxy(2) self.view(self.request) - self.request.META['HTTP_X_FORWARDED_FOR'] = '4.4.4.4, 1.1.1.1, 2.2.2.2' - assert self.view(self.request).status_code == 429 + spoofed = self.build_request('4.4.4.4, 1.1.1.1, 2.2.2.2') + assert self.view(spoofed).status_code == 429 class XffUniqueMachinesTest(XffTestingBase): def test_unique_clients_are_counted_independently_with_one_proxy(self): self.config_proxy(1) self.view(self.request) - self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 1.1.1.1, 7.7.7.7' - assert self.view(self.request).status_code == 200 + other_client = self.build_request('0.0.0.0, 1.1.1.1, 7.7.7.7') + assert self.view(other_client).status_code == 200 def test_unique_clients_are_counted_independently_with_two_proxies(self): self.config_proxy(2) self.view(self.request) - self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 7.7.7.7, 2.2.2.2' - assert self.view(self.request).status_code == 200 + other_client = self.build_request('0.0.0.0, 7.7.7.7, 2.2.2.2') + assert self.view(other_client).status_code == 200 class BaseThrottleTests(TestCase): diff --git a/tests/test_urlpatterns.py b/tests/test_urlpatterns.py index adcd0a7427..66a3b5e9f0 100644 --- a/tests/test_urlpatterns.py +++ b/tests/test_urlpatterns.py @@ -93,7 +93,7 @@ def test_format_suffix_django2(self): def test_format_suffix_django2_args(self): urlpatterns = [ path('convtest/', dummy_view), - re_path(r'^retest/(?P[0-9]+)$', dummy_view), + re_path(r'^retest/(?P[1-9]{2})$', dummy_view), ] test_paths = [ URLTestPath('/convtest/42', (), {'pk': 42}), @@ -145,10 +145,10 @@ def test_included_urls(self): def test_included_urls_mixed(self): nested_patterns = [ path('path/', dummy_view), - re_path(r'^re_path/(?P[0-9]+)$', dummy_view) + re_path(r'^re_path/(?P[0-9]{2})$', dummy_view) ] urlpatterns = [ - re_path(r'^pre_path/(?P[0-9]+)/', include(nested_patterns), {'foo': 'bar'}), + re_path(r'^pre_path/(?P[0-9]{2})/', include(nested_patterns), {'foo': 'bar'}), path('ppath//', include(nested_patterns), {'foo': 'bar'}), ] test_paths = [ @@ -185,7 +185,7 @@ def _test_allowed_formats(self, urlpatterns): def test_allowed_formats_re_path(self): urlpatterns = [ - re_path(r'^test$', dummy_view), + re_path(r'^[tes]{4}$', dummy_view), ] self._test_allowed_formats(urlpatterns) diff --git a/tests/test_versioning.py b/tests/test_versioning.py index b216461840..2c3535c3a4 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'), ]