From b83e9121f39822a873b45f2c42c88c7a59c64f87 Mon Sep 17 00:00:00 2001 From: Jair Henrique Date: Thu, 14 May 2020 10:48:14 -0300 Subject: [PATCH 1/4] Remove compat urls for Django < 2.0 (#7335) --- rest_framework/compat.py | 10 ---------- rest_framework/urlpatterns.py | 3 ++- tests/schemas/test_coreapi.py | 3 ++- tests/test_urlpatterns.py | 4 ++-- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 1de23bfaa..acace3467 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -212,16 +212,6 @@ else: return False -# Django 1.x url routing syntax. Remove when dropping Django 1.11 support. -try: - from django.urls import include, path, re_path, register_converter # noqa -except ImportError: - from django.conf.urls import include, url # noqa - path = None - register_converter = None - re_path = url - - # `separators` argument to `json.dumps()` differs between 2.x and 3.x # See: https://bugs.python.org/issue22767 SHORT_SEPARATORS = (',', ':') diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 831d344dd..9c82e1633 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -1,7 +1,8 @@ from django.conf.urls import include, url +from django.urls import path, register_converter from rest_framework.compat import ( - URLResolver, get_regex_pattern, is_route_pattern, path, register_converter + URLResolver, get_regex_pattern, is_route_pattern ) from rest_framework.settings import api_settings diff --git a/tests/schemas/test_coreapi.py b/tests/schemas/test_coreapi.py index a634d6968..77e18a9a1 100644 --- a/tests/schemas/test_coreapi.py +++ b/tests/schemas/test_coreapi.py @@ -5,11 +5,12 @@ from django.conf.urls import include, url from django.core.exceptions import PermissionDenied from django.http import Http404 from django.test import TestCase, override_settings +from django.urls import path from rest_framework import ( filters, generics, pagination, permissions, serializers ) -from rest_framework.compat import coreapi, coreschema, get_regex_pattern, path +from rest_framework.compat import coreapi, coreschema, get_regex_pattern from rest_framework.decorators import action, api_view, schema from rest_framework.request import Request from rest_framework.routers import DefaultRouter, SimpleRouter diff --git a/tests/test_urlpatterns.py b/tests/test_urlpatterns.py index 25cc0032e..51d269535 100644 --- a/tests/test_urlpatterns.py +++ b/tests/test_urlpatterns.py @@ -3,9 +3,9 @@ from collections import namedtuple from django.conf.urls import include, url from django.test import TestCase -from django.urls import Resolver404 +from django.urls import Resolver404, path, re_path -from rest_framework.compat import make_url_resolver, path, re_path +from rest_framework.compat import make_url_resolver from rest_framework.test import APIRequestFactory from rest_framework.urlpatterns import format_suffix_patterns From 65add6679d5eebe5c8baadb02b4c105da388e0e5 Mon Sep 17 00:00:00 2001 From: Jair Henrique Date: Thu, 14 May 2020 10:49:04 -0300 Subject: [PATCH 2/4] Remove unnecessary test skips (#7336) --- rest_framework/compat.py | 6 ----- tests/test_permissions.py | 46 +++++++++++++++++---------------------- 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index acace3467..ed1f43b62 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -2,8 +2,6 @@ The `compat` module provides support for backwards compatibility with older versions of Django/Python, and compatibility wrappers around optional packages. """ -import sys - from django.conf import settings from django.views.generic import View @@ -217,7 +215,3 @@ else: SHORT_SEPARATORS = (',', ':') LONG_SEPARATORS = (', ', ': ') INDENT_SEPARATORS = (',', ': ') - - -# Version Constants. -PY36 = sys.version_info >= (3, 6) diff --git a/tests/test_permissions.py b/tests/test_permissions.py index d445f271d..232c72dd2 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -3,7 +3,6 @@ import unittest from unittest import mock import django -import pytest from django.conf import settings from django.contrib.auth.models import AnonymousUser, Group, Permission, User from django.db import models @@ -14,7 +13,6 @@ from rest_framework import ( HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers, status, views ) -from rest_framework.compat import PY36 from rest_framework.routers import DefaultRouter from rest_framework.test import APIRequestFactory from tests.models import BasicModel @@ -607,7 +605,6 @@ class PermissionsCompositionTests(TestCase): ) assert composed_perm().has_permission(request, None) is True - @pytest.mark.skipif(not PY36, reason="assert_called_once() not available") def test_or_lazyness(self): request = factory.get('/1', format='json') request.user = AnonymousUser() @@ -616,19 +613,18 @@ class PermissionsCompositionTests(TestCase): with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: composed_perm = (permissions.AllowAny | permissions.IsAuthenticated) hasperm = composed_perm().has_permission(request, None) - self.assertIs(hasperm, True) - mock_allow.assert_called_once() + assert hasperm is True + assert mock_allow.call_count == 1 mock_deny.assert_not_called() with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow: with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: composed_perm = (permissions.IsAuthenticated | permissions.AllowAny) hasperm = composed_perm().has_permission(request, None) - self.assertIs(hasperm, True) - mock_deny.assert_called_once() - mock_allow.assert_called_once() + assert hasperm is True + assert mock_deny.call_count == 1 + assert mock_allow.call_count == 1 - @pytest.mark.skipif(not PY36, reason="assert_called_once() not available") def test_object_or_lazyness(self): request = factory.get('/1', format='json') request.user = AnonymousUser() @@ -637,19 +633,18 @@ class PermissionsCompositionTests(TestCase): with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny: composed_perm = (permissions.AllowAny | permissions.IsAuthenticated) hasperm = composed_perm().has_object_permission(request, None, None) - self.assertIs(hasperm, True) - mock_allow.assert_called_once() + assert hasperm is True + assert mock_allow.call_count == 1 mock_deny.assert_not_called() with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow: with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny: composed_perm = (permissions.IsAuthenticated | permissions.AllowAny) hasperm = composed_perm().has_object_permission(request, None, None) - self.assertIs(hasperm, True) - mock_deny.assert_called_once() - mock_allow.assert_called_once() + assert hasperm is True + assert mock_deny.call_count == 1 + assert mock_allow.call_count == 1 - @pytest.mark.skipif(not PY36, reason="assert_called_once() not available") def test_and_lazyness(self): request = factory.get('/1', format='json') request.user = AnonymousUser() @@ -658,19 +653,18 @@ class PermissionsCompositionTests(TestCase): with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: composed_perm = (permissions.AllowAny & permissions.IsAuthenticated) hasperm = composed_perm().has_permission(request, None) - self.assertIs(hasperm, False) - mock_allow.assert_called_once() - mock_deny.assert_called_once() + assert hasperm is False + assert mock_allow.call_count == 1 + assert mock_deny.call_count == 1 with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow: with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: composed_perm = (permissions.IsAuthenticated & permissions.AllowAny) hasperm = composed_perm().has_permission(request, None) - self.assertIs(hasperm, False) + assert hasperm is False + assert mock_deny.call_count == 1 mock_allow.assert_not_called() - mock_deny.assert_called_once() - @pytest.mark.skipif(not PY36, reason="assert_called_once() not available") def test_object_and_lazyness(self): request = factory.get('/1', format='json') request.user = AnonymousUser() @@ -679,14 +673,14 @@ class PermissionsCompositionTests(TestCase): with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny: composed_perm = (permissions.AllowAny & permissions.IsAuthenticated) hasperm = composed_perm().has_object_permission(request, None, None) - self.assertIs(hasperm, False) - mock_allow.assert_called_once() - mock_deny.assert_called_once() + assert hasperm is False + assert mock_allow.call_count == 1 + assert mock_deny.call_count == 1 with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow: with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny: composed_perm = (permissions.IsAuthenticated & permissions.AllowAny) hasperm = composed_perm().has_object_permission(request, None, None) - self.assertIs(hasperm, False) + assert hasperm is False + assert mock_deny.call_count == 1 mock_allow.assert_not_called() - mock_deny.assert_called_once() From bb795674f86828fc5f15d6d61501cc781811e053 Mon Sep 17 00:00:00 2001 From: Jair Henrique Date: Thu, 14 May 2020 15:31:38 -0300 Subject: [PATCH 3/4] Drop all compat support to Django < 2 urls (#7337) --- rest_framework/compat.py | 59 ---------------------------- rest_framework/schemas/generators.py | 4 +- rest_framework/urlpatterns.py | 14 +++---- tests/schemas/test_coreapi.py | 4 +- tests/test_filters.py | 2 - tests/test_routers.py | 13 +++--- tests/test_urlpatterns.py | 6 +-- 7 files changed, 18 insertions(+), 84 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index ed1f43b62..611068a62 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -5,65 +5,6 @@ versions of Django/Python, and compatibility wrappers around optional packages. from django.conf import settings from django.views.generic import View -try: - from django.urls import ( # noqa - URLPattern, - URLResolver, - ) -except ImportError: - # Will be removed in Django 2.0 - from django.urls import ( # noqa - RegexURLPattern as URLPattern, - RegexURLResolver as URLResolver, - ) - - -def get_original_route(urlpattern): - """ - Get the original route/regex that was typed in by the user into the path(), re_path() or url() directive. This - is in contrast with get_regex_pattern below, which for RoutePattern returns the raw regex generated from the path(). - """ - if hasattr(urlpattern, 'pattern'): - # Django 2.0 - return str(urlpattern.pattern) - else: - # Django < 2.0 - return urlpattern.regex.pattern - - -def get_regex_pattern(urlpattern): - """ - Get the raw regex out of the urlpattern's RegexPattern or RoutePattern. This is always a regular expression, - unlike get_original_route above. - """ - if hasattr(urlpattern, 'pattern'): - # Django 2.0 - return urlpattern.pattern.regex.pattern - else: - # Django < 2.0 - return urlpattern.regex.pattern - - -def is_route_pattern(urlpattern): - if hasattr(urlpattern, 'pattern'): - # Django 2.0 - from django.urls.resolvers import RoutePattern - return isinstance(urlpattern.pattern, RoutePattern) - else: - # Django < 2.0 - return False - - -def make_url_resolver(regex, urlpatterns): - try: - # Django 2.0 - from django.urls.resolvers import RegexPattern - return URLResolver(RegexPattern(regex), urlpatterns) - - except ImportError: - # Django < 2.0 - return URLResolver(regex, urlpatterns) - def unicode_http_header(value): # Coerce HTTP header value to unicode. diff --git a/rest_framework/schemas/generators.py b/rest_framework/schemas/generators.py index 77502d028..d3c6446aa 100644 --- a/rest_framework/schemas/generators.py +++ b/rest_framework/schemas/generators.py @@ -10,9 +10,9 @@ from django.conf import settings from django.contrib.admindocs.views import simplify_regex from django.core.exceptions import PermissionDenied from django.http import Http404 +from django.urls import URLPattern, URLResolver from rest_framework import exceptions -from rest_framework.compat import URLPattern, URLResolver, get_original_route from rest_framework.request import clone_request from rest_framework.settings import api_settings from rest_framework.utils.model_meta import _get_pk @@ -79,7 +79,7 @@ class EndpointEnumerator: api_endpoints = [] for pattern in patterns: - path_regex = prefix + get_original_route(pattern) + path_regex = prefix + str(pattern.pattern) if isinstance(pattern, URLPattern): path = self.get_path_from_regex(path_regex) callback = pattern.callback diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 9c82e1633..5b0bb4440 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -1,9 +1,7 @@ from django.conf.urls import include, url -from django.urls import path, register_converter +from django.urls import URLResolver, path, register_converter +from django.urls.resolvers import RoutePattern -from rest_framework.compat import ( - URLResolver, get_regex_pattern, is_route_pattern -) from rest_framework.settings import api_settings @@ -38,7 +36,7 @@ def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_r for urlpattern in urlpatterns: if isinstance(urlpattern, URLResolver): # Set of included URL patterns - regex = get_regex_pattern(urlpattern) + regex = urlpattern.pattern.regex.pattern namespace = urlpattern.namespace app_name = urlpattern.app_name kwargs = urlpattern.default_kwargs @@ -49,7 +47,7 @@ def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_r suffix_route) # if the original pattern was a RoutePattern we need to preserve it - if is_route_pattern(urlpattern): + if isinstance(urlpattern.pattern, RoutePattern): assert path is not None route = str(urlpattern.pattern) new_pattern = path(route, include((patterns, app_name), namespace), kwargs) @@ -59,7 +57,7 @@ def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_r ret.append(new_pattern) else: # Regular URL pattern - regex = get_regex_pattern(urlpattern).rstrip('$').rstrip('/') + suffix_pattern + regex = urlpattern.pattern.regex.pattern.rstrip('$').rstrip('/') + suffix_pattern view = urlpattern.callback kwargs = urlpattern.default_args name = urlpattern.name @@ -68,7 +66,7 @@ def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_r ret.append(urlpattern) # if the original pattern was a RoutePattern we need to preserve it - if is_route_pattern(urlpattern): + if isinstance(urlpattern.pattern, RoutePattern): assert path is not None assert suffix_route is not None route = str(urlpattern.pattern).rstrip('$').rstrip('/') + suffix_route diff --git a/tests/schemas/test_coreapi.py b/tests/schemas/test_coreapi.py index 77e18a9a1..403b3b634 100644 --- a/tests/schemas/test_coreapi.py +++ b/tests/schemas/test_coreapi.py @@ -10,7 +10,7 @@ from django.urls import path from rest_framework import ( filters, generics, pagination, permissions, serializers ) -from rest_framework.compat import coreapi, coreschema, get_regex_pattern +from rest_framework.compat import coreapi, coreschema from rest_framework.decorators import action, api_view, schema from rest_framework.request import Request from rest_framework.routers import DefaultRouter, SimpleRouter @@ -1079,7 +1079,7 @@ class SchemaGenerationExclusionTests(TestCase): inspector = EndpointEnumerator(self.patterns) # Not pretty. Mimics internals of EndpointEnumerator to put should_include_endpoint under test - pairs = [(inspector.get_path_from_regex(get_regex_pattern(pattern)), pattern.callback) + pairs = [(inspector.get_path_from_regex(pattern.pattern.regex.pattern), pattern.callback) for pattern in self.patterns] should_include = [ diff --git a/tests/test_filters.py b/tests/test_filters.py index e69537666..567e5f83f 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,7 +1,6 @@ import datetime from importlib import reload as reload_module -import django import pytest from django.core.exceptions import ImproperlyConfigured from django.db import models @@ -191,7 +190,6 @@ class SearchFilterTests(TestCase): assert terms == ['asdf'] - @pytest.mark.skipif(django.VERSION[:2] < (2, 2), reason="requires django 2.2 or higher") def test_search_field_with_additional_transforms(self): from django.test.utils import register_lookup diff --git a/tests/test_routers.py b/tests/test_routers.py index ff927ff33..007cb4768 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -8,7 +8,6 @@ from django.test import TestCase, override_settings from django.urls import resolve, reverse from rest_framework import permissions, serializers, viewsets -from rest_framework.compat import get_regex_pattern from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.routers import DefaultRouter, SimpleRouter @@ -192,8 +191,7 @@ class TestCustomLookupFields(URLPatternsTestCase, TestCase): def test_custom_lookup_field_route(self): detail_route = notes_router.urls[-1] - detail_url_pattern = get_regex_pattern(detail_route) - assert '' in detail_url_pattern + assert '' in detail_route.pattern.regex.pattern def test_retrieve_lookup_field_list_view(self): response = self.client.get('/example/notes/') @@ -229,7 +227,7 @@ class TestLookupValueRegex(TestCase): def test_urls_limited_by_lookup_value_regex(self): expected = ['^notes/$', '^notes/(?P[0-9a-f]{32})/$'] for idx in range(len(expected)): - assert expected[idx] == get_regex_pattern(self.urls[idx]) + assert expected[idx] == self.urls[idx].pattern.regex.pattern @override_settings(ROOT_URLCONF='tests.test_routers') @@ -249,8 +247,7 @@ class TestLookupUrlKwargs(URLPatternsTestCase, TestCase): def test_custom_lookup_url_kwarg_route(self): detail_route = kwarged_notes_router.urls[-1] - detail_url_pattern = get_regex_pattern(detail_route) - assert '^notes/(?P' in detail_url_pattern + assert '^notes/(?P' in detail_route.pattern.regex.pattern def test_retrieve_lookup_url_kwarg_detail_view(self): response = self.client.get('/example2/notes/fo/') @@ -273,7 +270,7 @@ class TestTrailingSlashIncluded(TestCase): def test_urls_have_trailing_slash_by_default(self): expected = ['^notes/$', '^notes/(?P[^/.]+)/$'] for idx in range(len(expected)): - assert expected[idx] == get_regex_pattern(self.urls[idx]) + assert expected[idx] == self.urls[idx].pattern.regex.pattern class TestTrailingSlashRemoved(TestCase): @@ -288,7 +285,7 @@ class TestTrailingSlashRemoved(TestCase): def test_urls_can_have_trailing_slash_removed(self): expected = ['^notes$', '^notes/(?P[^/.]+)$'] for idx in range(len(expected)): - assert expected[idx] == get_regex_pattern(self.urls[idx]) + assert expected[idx] == self.urls[idx].pattern.regex.pattern class TestNameableRoot(TestCase): diff --git a/tests/test_urlpatterns.py b/tests/test_urlpatterns.py index 51d269535..ec19494b0 100644 --- a/tests/test_urlpatterns.py +++ b/tests/test_urlpatterns.py @@ -3,9 +3,9 @@ from collections import namedtuple from django.conf.urls import include, url from django.test import TestCase -from django.urls import Resolver404, path, re_path +from django.urls import Resolver404, URLResolver, path, re_path +from django.urls.resolvers import RegexPattern -from rest_framework.compat import make_url_resolver from rest_framework.test import APIRequestFactory from rest_framework.urlpatterns import format_suffix_patterns @@ -28,7 +28,7 @@ class FormatSuffixTests(TestCase): urlpatterns = format_suffix_patterns(urlpatterns, allowed=allowed) except Exception: self.fail("Failed to apply `format_suffix_patterns` on the supplied urlpatterns") - resolver = make_url_resolver(r'^/', urlpatterns) + resolver = URLResolver(RegexPattern(r'^/'), urlpatterns) for test_path in test_paths: try: test_path, expected_resolved = test_path From acbd9d8222e763c7f9c7dc2de23c430c702e06d4 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Fri, 15 May 2020 13:40:47 +0600 Subject: [PATCH 4/4] django 3.1 alpha on matrix (#7334) * django 3.1 alpha on matrix * django 3.1 alpha on matrix --- .travis.yml | 3 +++ setup.py | 1 + tox.ini | 3 +++ 3 files changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index f1ec689f7..39efaf4fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,13 +9,16 @@ matrix: - { python: "3.6", env: DJANGO=2.2 } - { python: "3.6", env: DJANGO=3.0 } + - { python: "3.6", env: DJANGO=3.1 } - { python: "3.6", env: DJANGO=master } - { python: "3.7", env: DJANGO=2.2 } - { python: "3.7", env: DJANGO=3.0 } + - { python: "3.7", env: DJANGO=3.1 } - { python: "3.7", env: DJANGO=master } - { python: "3.8", env: DJANGO=3.0 } + - { python: "3.8", env: DJANGO=3.1 } - { python: "3.8", env: DJANGO=master } - { python: "3.8", env: TOXENV=base } diff --git a/setup.py b/setup.py index 99826b4d0..38e680e10 100755 --- a/setup.py +++ b/setup.py @@ -91,6 +91,7 @@ setup( 'Framework :: Django', 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.0', + 'Framework :: Django :: 3.1', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', diff --git a/tox.ini b/tox.ini index e5b8b6402..190865f23 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = {py35,py36,py37}-django22, {py36,py37,py38}-django30, + {py36,py37,py38}-django31, {py36,py37,py38}-djangomaster, base,dist,lint,docs, @@ -9,6 +10,7 @@ envlist = DJANGO = 2.2: django22 3.0: django30 + 3.1: django31 master: djangomaster [testenv] @@ -20,6 +22,7 @@ setenv = deps = django22: Django>=2.2,<3.0 django30: Django>=3.0,<3.1 + django31: Django>=3.1a1,<3.2 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt