This commit is contained in:
Asif Saif Uddin (Auvi) 2020-05-15 17:32:57 +06:00
commit aa0a4557d7
11 changed files with 46 additions and 125 deletions

View File

@ -9,13 +9,16 @@ matrix:
- { python: "3.6", env: DJANGO=2.2 } - { python: "3.6", env: DJANGO=2.2 }
- { python: "3.6", env: DJANGO=3.0 } - { python: "3.6", env: DJANGO=3.0 }
- { python: "3.6", env: DJANGO=3.1 }
- { python: "3.6", env: DJANGO=master } - { python: "3.6", env: DJANGO=master }
- { python: "3.7", env: DJANGO=2.2 } - { python: "3.7", env: DJANGO=2.2 }
- { python: "3.7", env: DJANGO=3.0 } - { python: "3.7", env: DJANGO=3.0 }
- { python: "3.7", env: DJANGO=3.1 }
- { python: "3.7", env: DJANGO=master } - { python: "3.7", env: DJANGO=master }
- { python: "3.8", env: DJANGO=3.0 } - { python: "3.8", env: DJANGO=3.0 }
- { python: "3.8", env: DJANGO=3.1 }
- { python: "3.8", env: DJANGO=master } - { python: "3.8", env: DJANGO=master }
- { python: "3.8", env: TOXENV=base } - { python: "3.8", env: TOXENV=base }

View File

@ -2,70 +2,9 @@
The `compat` module provides support for backwards compatibility with older The `compat` module provides support for backwards compatibility with older
versions of Django/Python, and compatibility wrappers around optional packages. versions of Django/Python, and compatibility wrappers around optional packages.
""" """
import sys
from django.conf import settings from django.conf import settings
from django.views.generic import View 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): def unicode_http_header(value):
# Coerce HTTP header value to unicode. # Coerce HTTP header value to unicode.
@ -212,22 +151,8 @@ else:
return False 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 # `separators` argument to `json.dumps()` differs between 2.x and 3.x
# See: https://bugs.python.org/issue22767 # See: https://bugs.python.org/issue22767
SHORT_SEPARATORS = (',', ':') SHORT_SEPARATORS = (',', ':')
LONG_SEPARATORS = (', ', ': ') LONG_SEPARATORS = (', ', ': ')
INDENT_SEPARATORS = (',', ': ') INDENT_SEPARATORS = (',', ': ')
# Version Constants.
PY36 = sys.version_info >= (3, 6)

View File

@ -10,9 +10,9 @@ from django.conf import settings
from django.contrib.admindocs.views import simplify_regex from django.contrib.admindocs.views import simplify_regex
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404 from django.http import Http404
from django.urls import URLPattern, URLResolver
from rest_framework import exceptions 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.request import clone_request
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.utils.model_meta import _get_pk from rest_framework.utils.model_meta import _get_pk
@ -79,7 +79,7 @@ class EndpointEnumerator:
api_endpoints = [] api_endpoints = []
for pattern in patterns: for pattern in patterns:
path_regex = prefix + get_original_route(pattern) path_regex = prefix + str(pattern.pattern)
if isinstance(pattern, URLPattern): if isinstance(pattern, URLPattern):
path = self.get_path_from_regex(path_regex) path = self.get_path_from_regex(path_regex)
callback = pattern.callback callback = pattern.callback

View File

@ -1,8 +1,7 @@
from django.conf.urls import include, url from django.conf.urls import include, url
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, path, register_converter
)
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -37,7 +36,7 @@ def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_r
for urlpattern in urlpatterns: for urlpattern in urlpatterns:
if isinstance(urlpattern, URLResolver): if isinstance(urlpattern, URLResolver):
# Set of included URL patterns # Set of included URL patterns
regex = get_regex_pattern(urlpattern) regex = urlpattern.pattern.regex.pattern
namespace = urlpattern.namespace namespace = urlpattern.namespace
app_name = urlpattern.app_name app_name = urlpattern.app_name
kwargs = urlpattern.default_kwargs kwargs = urlpattern.default_kwargs
@ -48,7 +47,7 @@ def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_r
suffix_route) suffix_route)
# if the original pattern was a RoutePattern we need to preserve it # 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 path is not None
route = str(urlpattern.pattern) route = str(urlpattern.pattern)
new_pattern = path(route, include((patterns, app_name), namespace), kwargs) new_pattern = path(route, include((patterns, app_name), namespace), kwargs)
@ -58,7 +57,7 @@ def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_r
ret.append(new_pattern) ret.append(new_pattern)
else: else:
# Regular URL pattern # Regular URL pattern
regex = get_regex_pattern(urlpattern).rstrip('$').rstrip('/') + suffix_pattern regex = urlpattern.pattern.regex.pattern.rstrip('$').rstrip('/') + suffix_pattern
view = urlpattern.callback view = urlpattern.callback
kwargs = urlpattern.default_args kwargs = urlpattern.default_args
name = urlpattern.name name = urlpattern.name
@ -67,7 +66,7 @@ def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_r
ret.append(urlpattern) ret.append(urlpattern)
# if the original pattern was a RoutePattern we need to preserve it # 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 path is not None
assert suffix_route is not None assert suffix_route is not None
route = str(urlpattern.pattern).rstrip('$').rstrip('/') + suffix_route route = str(urlpattern.pattern).rstrip('$').rstrip('/') + suffix_route

View File

@ -91,6 +91,7 @@ setup(
'Framework :: Django', 'Framework :: Django',
'Framework :: Django :: 2.2', 'Framework :: Django :: 2.2',
'Framework :: Django :: 3.0', 'Framework :: Django :: 3.0',
'Framework :: Django :: 3.1',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',

View File

@ -5,11 +5,12 @@ from django.conf.urls import include, url
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404 from django.http import Http404
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.urls import path
from rest_framework import ( from rest_framework import (
filters, generics, pagination, permissions, serializers filters, generics, pagination, permissions, serializers
) )
from rest_framework.compat import coreapi, coreschema, get_regex_pattern, path from rest_framework.compat import coreapi, coreschema
from rest_framework.decorators import action, api_view, schema from rest_framework.decorators import action, api_view, schema
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.routers import DefaultRouter, SimpleRouter from rest_framework.routers import DefaultRouter, SimpleRouter
@ -1078,7 +1079,7 @@ class SchemaGenerationExclusionTests(TestCase):
inspector = EndpointEnumerator(self.patterns) inspector = EndpointEnumerator(self.patterns)
# Not pretty. Mimics internals of EndpointEnumerator to put should_include_endpoint under test # 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] for pattern in self.patterns]
should_include = [ should_include = [

View File

@ -1,7 +1,6 @@
import datetime import datetime
from importlib import reload as reload_module from importlib import reload as reload_module
import django
import pytest import pytest
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
@ -191,7 +190,6 @@ class SearchFilterTests(TestCase):
assert terms == ['asdf'] 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): def test_search_field_with_additional_transforms(self):
from django.test.utils import register_lookup from django.test.utils import register_lookup

View File

@ -3,7 +3,6 @@ import unittest
from unittest import mock from unittest import mock
import django import django
import pytest
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AnonymousUser, Group, Permission, User from django.contrib.auth.models import AnonymousUser, Group, Permission, User
from django.db import models from django.db import models
@ -14,7 +13,6 @@ from rest_framework import (
HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers, HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers,
status, views status, views
) )
from rest_framework.compat import PY36
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from tests.models import BasicModel from tests.models import BasicModel
@ -607,7 +605,6 @@ class PermissionsCompositionTests(TestCase):
) )
assert composed_perm().has_permission(request, None) is True 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): def test_or_lazyness(self):
request = factory.get('/1', format='json') request = factory.get('/1', format='json')
request.user = AnonymousUser() request.user = AnonymousUser()
@ -616,19 +613,18 @@ class PermissionsCompositionTests(TestCase):
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
composed_perm = (permissions.AllowAny | permissions.IsAuthenticated) composed_perm = (permissions.AllowAny | permissions.IsAuthenticated)
hasperm = composed_perm().has_permission(request, None) hasperm = composed_perm().has_permission(request, None)
self.assertIs(hasperm, True) assert hasperm is True
mock_allow.assert_called_once() assert mock_allow.call_count == 1
mock_deny.assert_not_called() mock_deny.assert_not_called()
with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow: 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: with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
composed_perm = (permissions.IsAuthenticated | permissions.AllowAny) composed_perm = (permissions.IsAuthenticated | permissions.AllowAny)
hasperm = composed_perm().has_permission(request, None) hasperm = composed_perm().has_permission(request, None)
self.assertIs(hasperm, True) assert hasperm is True
mock_deny.assert_called_once() assert mock_deny.call_count == 1
mock_allow.assert_called_once() assert mock_allow.call_count == 1
@pytest.mark.skipif(not PY36, reason="assert_called_once() not available")
def test_object_or_lazyness(self): def test_object_or_lazyness(self):
request = factory.get('/1', format='json') request = factory.get('/1', format='json')
request.user = AnonymousUser() 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: with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
composed_perm = (permissions.AllowAny | permissions.IsAuthenticated) composed_perm = (permissions.AllowAny | permissions.IsAuthenticated)
hasperm = composed_perm().has_object_permission(request, None, None) hasperm = composed_perm().has_object_permission(request, None, None)
self.assertIs(hasperm, True) assert hasperm is True
mock_allow.assert_called_once() assert mock_allow.call_count == 1
mock_deny.assert_not_called() 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.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: with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
composed_perm = (permissions.IsAuthenticated | permissions.AllowAny) composed_perm = (permissions.IsAuthenticated | permissions.AllowAny)
hasperm = composed_perm().has_object_permission(request, None, None) hasperm = composed_perm().has_object_permission(request, None, None)
self.assertIs(hasperm, True) assert hasperm is True
mock_deny.assert_called_once() assert mock_deny.call_count == 1
mock_allow.assert_called_once() assert mock_allow.call_count == 1
@pytest.mark.skipif(not PY36, reason="assert_called_once() not available")
def test_and_lazyness(self): def test_and_lazyness(self):
request = factory.get('/1', format='json') request = factory.get('/1', format='json')
request.user = AnonymousUser() request.user = AnonymousUser()
@ -658,19 +653,18 @@ class PermissionsCompositionTests(TestCase):
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
composed_perm = (permissions.AllowAny & permissions.IsAuthenticated) composed_perm = (permissions.AllowAny & permissions.IsAuthenticated)
hasperm = composed_perm().has_permission(request, None) hasperm = composed_perm().has_permission(request, None)
self.assertIs(hasperm, False) assert hasperm is False
mock_allow.assert_called_once() assert mock_allow.call_count == 1
mock_deny.assert_called_once() 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.AllowAny, 'has_permission', return_value=True) as mock_allow:
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
composed_perm = (permissions.IsAuthenticated & permissions.AllowAny) composed_perm = (permissions.IsAuthenticated & permissions.AllowAny)
hasperm = composed_perm().has_permission(request, None) 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_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): def test_object_and_lazyness(self):
request = factory.get('/1', format='json') request = factory.get('/1', format='json')
request.user = AnonymousUser() 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: with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
composed_perm = (permissions.AllowAny & permissions.IsAuthenticated) composed_perm = (permissions.AllowAny & permissions.IsAuthenticated)
hasperm = composed_perm().has_object_permission(request, None, None) hasperm = composed_perm().has_object_permission(request, None, None)
self.assertIs(hasperm, False) assert hasperm is False
mock_allow.assert_called_once() assert mock_allow.call_count == 1
mock_deny.assert_called_once() 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.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: with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
composed_perm = (permissions.IsAuthenticated & permissions.AllowAny) composed_perm = (permissions.IsAuthenticated & permissions.AllowAny)
hasperm = composed_perm().has_object_permission(request, None, None) 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_allow.assert_not_called()
mock_deny.assert_called_once()

View File

@ -8,7 +8,6 @@ from django.test import TestCase, override_settings
from django.urls import resolve, reverse from django.urls import resolve, reverse
from rest_framework import permissions, serializers, viewsets from rest_framework import permissions, serializers, viewsets
from rest_framework.compat import get_regex_pattern
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.routers import DefaultRouter, SimpleRouter from rest_framework.routers import DefaultRouter, SimpleRouter
@ -192,8 +191,7 @@ class TestCustomLookupFields(URLPatternsTestCase, TestCase):
def test_custom_lookup_field_route(self): def test_custom_lookup_field_route(self):
detail_route = notes_router.urls[-1] detail_route = notes_router.urls[-1]
detail_url_pattern = get_regex_pattern(detail_route) assert '<uuid>' in detail_route.pattern.regex.pattern
assert '<uuid>' in detail_url_pattern
def test_retrieve_lookup_field_list_view(self): def test_retrieve_lookup_field_list_view(self):
response = self.client.get('/example/notes/') response = self.client.get('/example/notes/')
@ -229,7 +227,7 @@ class TestLookupValueRegex(TestCase):
def test_urls_limited_by_lookup_value_regex(self): def test_urls_limited_by_lookup_value_regex(self):
expected = ['^notes/$', '^notes/(?P<uuid>[0-9a-f]{32})/$'] expected = ['^notes/$', '^notes/(?P<uuid>[0-9a-f]{32})/$']
for idx in range(len(expected)): 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') @override_settings(ROOT_URLCONF='tests.test_routers')
@ -249,8 +247,7 @@ class TestLookupUrlKwargs(URLPatternsTestCase, TestCase):
def test_custom_lookup_url_kwarg_route(self): def test_custom_lookup_url_kwarg_route(self):
detail_route = kwarged_notes_router.urls[-1] detail_route = kwarged_notes_router.urls[-1]
detail_url_pattern = get_regex_pattern(detail_route) assert '^notes/(?P<text>' in detail_route.pattern.regex.pattern
assert '^notes/(?P<text>' in detail_url_pattern
def test_retrieve_lookup_url_kwarg_detail_view(self): def test_retrieve_lookup_url_kwarg_detail_view(self):
response = self.client.get('/example2/notes/fo/') response = self.client.get('/example2/notes/fo/')
@ -273,7 +270,7 @@ class TestTrailingSlashIncluded(TestCase):
def test_urls_have_trailing_slash_by_default(self): def test_urls_have_trailing_slash_by_default(self):
expected = ['^notes/$', '^notes/(?P<pk>[^/.]+)/$'] expected = ['^notes/$', '^notes/(?P<pk>[^/.]+)/$']
for idx in range(len(expected)): 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): class TestTrailingSlashRemoved(TestCase):
@ -288,7 +285,7 @@ class TestTrailingSlashRemoved(TestCase):
def test_urls_can_have_trailing_slash_removed(self): def test_urls_can_have_trailing_slash_removed(self):
expected = ['^notes$', '^notes/(?P<pk>[^/.]+)$'] expected = ['^notes$', '^notes/(?P<pk>[^/.]+)$']
for idx in range(len(expected)): 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): class TestNameableRoot(TestCase):

View File

@ -3,9 +3,9 @@ from collections import namedtuple
from django.conf.urls import include, url from django.conf.urls import include, url
from django.test import TestCase from django.test import TestCase
from django.urls import Resolver404 from django.urls import Resolver404, URLResolver, path, re_path
from django.urls.resolvers import RegexPattern
from rest_framework.compat import make_url_resolver, path, re_path
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
@ -28,7 +28,7 @@ class FormatSuffixTests(TestCase):
urlpatterns = format_suffix_patterns(urlpatterns, allowed=allowed) urlpatterns = format_suffix_patterns(urlpatterns, allowed=allowed)
except Exception: except Exception:
self.fail("Failed to apply `format_suffix_patterns` on the supplied urlpatterns") 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: for test_path in test_paths:
try: try:
test_path, expected_resolved = test_path test_path, expected_resolved = test_path

View File

@ -2,6 +2,7 @@
envlist = envlist =
{py35,py36,py37}-django22, {py35,py36,py37}-django22,
{py36,py37,py38}-django30, {py36,py37,py38}-django30,
{py36,py37,py38}-django31,
{py36,py37,py38}-djangomaster, {py36,py37,py38}-djangomaster,
base,dist,lint,docs, base,dist,lint,docs,
@ -9,6 +10,7 @@ envlist =
DJANGO = DJANGO =
2.2: django22 2.2: django22
3.0: django30 3.0: django30
3.1: django31
master: djangomaster master: djangomaster
[testenv] [testenv]
@ -20,6 +22,7 @@ setenv =
deps = deps =
django22: Django>=2.2,<3.0 django22: Django>=2.2,<3.0
django30: Django>=3.0,<3.1 django30: Django>=3.0,<3.1
django31: Django>=3.1a1,<3.2
djangomaster: https://github.com/django/django/archive/master.tar.gz djangomaster: https://github.com/django/django/archive/master.tar.gz
-rrequirements/requirements-testing.txt -rrequirements/requirements-testing.txt
-rrequirements/requirements-optionals.txt -rrequirements/requirements-optionals.txt