Compare commits

...

13 Commits

Author SHA1 Message Date
Cristi Vijdea bc931677dc Add 1.16.1 changelog 2019-07-16 20:59:36 +03:00
tfranzel 1904b0499e Fix imports for DRF 3.10 (#408)
Fixes #410
Fixes #411
2019-07-16 20:39:44 +03:00
johnthagen 1e380fe68b Fix variable formatting (#406) 2019-07-15 12:14:37 +03:00
Étienne Noss 6417bb3770 Handle enum type for nested ChoiceFields (#400) 2019-07-15 12:14:23 +03:00
Cristi Vîjdea e9f27442fc Remove unused import 2019-06-13 16:58:41 +03:00
Cristi Vîjdea cf8b912c10 Align tox matrix to python version 2019-06-13 16:57:15 +03:00
Cristi Vîjdea 3a37c4a019 Drop Django 2.0 and DRF 3.7 support 2019-06-13 16:49:46 +03:00
Cristi Vîjdea 8acab171ea Fix isort
[ci skip]
2019-06-13 16:43:16 +03:00
Cristi Vîjdea acc204e4ea Add test for #382 2019-06-13 16:34:23 +03:00
Cristi Vîjdea 1635e5e095 Add 1.16.0 changelog 2019-06-13 13:48:45 +03:00
Cristi Vîjdea 753be1a8bd Fix potential issubclass crash 2019-06-13 13:43:16 +03:00
Pavel White 2656696a0f generators: support custom ReferenceResolver class (#350) 2019-06-13 03:38:17 +03:00
Hugo Duroux a083d3cf7c Add operation_keys to SwaggerAutoSchema constructor (#355) 2019-06-13 03:37:06 +03:00
19 changed files with 153 additions and 44 deletions
+3 -3
View File
@@ -13,8 +13,8 @@ Generate **real** Swagger/OpenAPI 2.0 specifications from a Django Rest Framewor
Compatible with Compatible with
- **Django Rest Framework**: 3.7.7, 3.8, 3.9 - **Django Rest Framework**: 3.8, 3.9
- **Django**: 1.11, 2.0, 2.1, 2.2 - **Django**: 1.11, 2.1, 2.2
- **Python**: 2.7, 3.5, 3.6, 3.7 - **Python**: 2.7, 3.5, 3.6, 3.7
Only the latest patch version of each ``major.minor`` series of Python, Django and Django REST Framework is supported. Only the latest patch version of each ``major.minor`` series of Python, Django and Django REST Framework is supported.
@@ -235,7 +235,7 @@ Offline
^^^^^^^ ^^^^^^^
If your schema is not accessible from the internet, you can run a local copy of If your schema is not accessible from the internet, you can run a local copy of
`swagger-validator <https://hub.docker.com/r/swaggerapi/swagger-validator/>`_ and set the `VALIDATOR_URL` accordingly: `swagger-validator <https://hub.docker.com/r/swaggerapi/swagger-validator/>`_ and set the ``VALIDATOR_URL`` accordingly:
.. code:: python .. code:: python
+20 -1
View File
@@ -2,6 +2,26 @@
Changelog Changelog
######### #########
**********
**1.16.1**
**********
*Release date: Jul 16, 2019*
- **FIXED:** fixed DRF 3.10 compatibility (:pr:`408`, :issue:`410`, :issue:`411`)
- **IMPROVED:** better enum type detection for nested `ChoiceField`s (:pr:`400`)
**********
**1.16.0**
**********
*Release date: Jun 13, 2019*
- **ADDED:** added `reference_resolver_class` attribute hook to `SwaggerAutoSchema` (:pr:`350`)
- **ADDED:** added `operation_keys` attribute to `SwaggerAutoSchema`, along with `__init__` parameter (:pr:`355`)
- **FIXED:** fixed potential crash on `issubclass` check without `isclass` check
********** **********
**1.15.1** **1.15.1**
********** **********
@@ -15,7 +35,6 @@ Changelog
**Python 3.4 support has been dropped!** **Python 3.4 support has been dropped!**
********** **********
**1.15.0** **1.15.0**
********** **********
+3 -3
View File
@@ -87,14 +87,14 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
* for ``ViewSet``, ``GenericViewSet``, ``ModelViewSet``, because each viewset corresponds to multiple **paths**, you have * for ``ViewSet``, ``GenericViewSet``, ``ModelViewSet``, because each viewset corresponds to multiple **paths**, you have
to decorate the *action methods*, i.e. ``list``, ``create``, ``retrieve``, etc. |br| to decorate the *action methods*, i.e. ``list``, ``create``, ``retrieve``, etc. |br|
Additionally, ``@action``\ s, ``@list_route``\ s or ``@detail_route``\ s defined on the viewset, like function based Additionally, ``@action``\ s defined on the viewset, like function based api views, can respond to multiple HTTP
api views, can respond to multiple HTTP methods and thus have multiple operations that must be decorated separately: methods and thus have multiple operations that must be decorated separately:
.. code-block:: python .. code-block:: python
class ArticleViewSet(viewsets.ModelViewSet): class ArticleViewSet(viewsets.ModelViewSet):
# method or 'methods' can be skipped because the list_route only handles a single method (GET) # method or 'methods' can be skipped because the action only handles a single method (GET)
@swagger_auto_schema(operation_description='GET /articles/today/') @swagger_auto_schema(operation_description='GET /articles/today/')
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
def today(self, request): def today(self, request):
+1 -1
View File
@@ -5,5 +5,5 @@ inflection>=0.3.1
six>=1.10.0 six>=1.10.0
uritemplate>=3.0.0 uritemplate>=3.0.0
djangorestframework>=3.7.7 djangorestframework>=3.8
Django>=1.11.7 Django>=1.11.7
+2
View File
@@ -5,5 +5,7 @@ pytest-cov>=2.6.0
pytest-xdist>=1.25.0 pytest-xdist>=1.25.0
pytest-django>=3.4.4 pytest-django>=3.4.4
datadiff==2.0.0 datadiff==2.0.0
psycopg2-binary==2.8.3
django-fake-model==0.1.4
-r testproj.txt -r testproj.txt
+3 -1
View File
@@ -3,7 +3,9 @@ Pillow>=4.3.0
django-cors-headers>=2.1.0 django-cors-headers>=2.1.0
django-filter>=1.1.0,<2.0; python_version == "2.7" django-filter>=1.1.0,<2.0; python_version == "2.7"
django-filter>=1.1.0; python_version >= "3.5" django-filter>=1.1.0; python_version >= "3.5"
djangorestframework-camel-case>=0.2.0 #djangorestframework-camel-case>=0.2.0
# tempory replacement of broken lib
-e git+https://github.com/tfranzel/djangorestframework-camel-case.git@bd556d38fa7382acadfe91d93d92d99c663248a9#egg=djangorestframework_camel_case
djangorestframework-recursive>=0.1.2 djangorestframework-recursive>=0.1.2
dj-database-url>=0.4.2 dj-database-url>=0.4.2
user_agents>=1.1.0 user_agents>=1.1.0
+1 -1
View File
@@ -1 +1 @@
python-3.7.1 python-3.7.3
+14 -4
View File
@@ -4,14 +4,23 @@ import re
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
import uritemplate import uritemplate
import rest_framework
from coreapi.compat import urlparse from coreapi.compat import urlparse
from rest_framework import versioning from rest_framework import versioning
from rest_framework.compat import URLPattern, URLResolver, get_original_route from rest_framework.compat import URLPattern, URLResolver, get_original_route
from rest_framework.schemas.generators import EndpointEnumerator as _EndpointEnumerator from rest_framework.schemas.generators import EndpointEnumerator as _EndpointEnumerator
from rest_framework.schemas.generators import SchemaGenerator, endpoint_ordering, get_pk_name from rest_framework.schemas.generators import endpoint_ordering, get_pk_name
from rest_framework.schemas.inspectors import get_pk_description
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from packaging.version import Version
if Version(rest_framework.__version__) < Version('3.10'):
from rest_framework.schemas.generators import SchemaGenerator
from rest_framework.schemas.inspectors import get_pk_description
else:
from rest_framework.schemas import SchemaGenerator
from rest_framework.schemas.utils import get_pk_description
from . import openapi from . import openapi
from .app_settings import swagger_settings from .app_settings import swagger_settings
from .errors import SwaggerGenerationError from .errors import SwaggerGenerationError
@@ -160,6 +169,7 @@ class OpenAPISchemaGenerator(object):
Method implementations shamelessly stolen and adapted from rest-framework ``SchemaGenerator``. Method implementations shamelessly stolen and adapted from rest-framework ``SchemaGenerator``.
""" """
endpoint_enumerator_class = EndpointEnumerator endpoint_enumerator_class = EndpointEnumerator
reference_resolver_class = ReferenceResolver
def __init__(self, info, version='', url=None, patterns=None, urlconf=None): def __init__(self, info, version='', url=None, patterns=None, urlconf=None):
""" """
@@ -238,7 +248,7 @@ class OpenAPISchemaGenerator(object):
:rtype: openapi.Swagger :rtype: openapi.Swagger
""" """
endpoints = self.get_endpoints(request) endpoints = self.get_endpoints(request)
components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS, force_init=True) components = self.reference_resolver_class(openapi.SCHEMA_DEFINITIONS, force_init=True)
self.consumes = get_consumes(api_settings.DEFAULT_PARSER_CLASSES) self.consumes = get_consumes(api_settings.DEFAULT_PARSER_CLASSES)
self.produces = get_produces(api_settings.DEFAULT_RENDERER_CLASSES) self.produces = get_produces(api_settings.DEFAULT_RENDERER_CLASSES)
paths, prefix = self.get_paths(endpoints, components, request, public) paths, prefix = self.get_paths(endpoints, components, request, public)
@@ -440,7 +450,7 @@ class OpenAPISchemaGenerator(object):
if view_inspector_cls is None: if view_inspector_cls is None:
return None return None
view_inspector = view_inspector_cls(view, path, method, components, request, overrides) view_inspector = view_inspector_cls(view, path, method, components, request, overrides, operation_keys)
operation = view_inspector.get_operation(operation_keys) operation = view_inspector.get_operation(operation_keys)
if operation is None: if operation is None:
return None return None
+6 -2
View File
@@ -610,7 +610,7 @@ class SerializerMethodFieldInspector(FieldInspector):
# look for Python 3.5+ style type hinting of the return value # look for Python 3.5+ style type hinting of the return value
hint_class = inspect_signature(method).return_annotation hint_class = inspect_signature(method).return_annotation
if not issubclass(hint_class, inspect._empty): if inspect.isclass(hint_class) and not issubclass(hint_class, inspect._empty):
type_info = get_basic_type_info_from_hint(hint_class) type_info = get_basic_type_info_from_hint(hint_class)
if type_info is not None: if type_info is not None:
@@ -656,7 +656,11 @@ class ChoiceFieldInspector(FieldInspector):
serializer = get_parent_serializer(field) serializer = get_parent_serializer(field)
if isinstance(serializer, serializers.ModelSerializer): if isinstance(serializer, serializers.ModelSerializer):
model = getattr(getattr(serializer, 'Meta'), 'model') model = getattr(getattr(serializer, 'Meta'), 'model')
model_field = get_model_field(model, field.source) # Use the parent source for nested fields
model_field = get_model_field(model, field.source or field.parent.source)
# If the field has a base_field its type must be used
if getattr(model_field, "base_field", None):
model_field = model_field.base_field
if model_field: if model_field:
model_type = get_basic_type_info(model_field) model_type = get_basic_type_info(model_field)
if model_type: if model_type:
+11 -4
View File
@@ -17,12 +17,15 @@ logger = logging.getLogger(__name__)
class SwaggerAutoSchema(ViewInspector): class SwaggerAutoSchema(ViewInspector):
def __init__(self, view, path, method, components, request, overrides): def __init__(self, view, path, method, components, request, overrides, operation_keys=None):
super(SwaggerAutoSchema, self).__init__(view, path, method, components, request, overrides) super(SwaggerAutoSchema, self).__init__(view, path, method, components, request, overrides)
self._sch = AutoSchema() self._sch = AutoSchema()
self._sch.view = view self._sch.view = view
self.operation_keys = operation_keys
def get_operation(self, operation_keys=None):
operation_keys = operation_keys or self.operation_keys
def get_operation(self, operation_keys):
consumes = self.get_consumes() consumes = self.get_consumes()
produces = self.get_produces() produces = self.get_produces()
@@ -300,7 +303,7 @@ class SwaggerAutoSchema(ViewInspector):
return natural_parameters + serializer_parameters return natural_parameters + serializer_parameters
def get_operation_id(self, operation_keys): def get_operation_id(self, operation_keys=None):
"""Return an unique ID for this operation. The ID must be unique across """Return an unique ID for this operation. The ID must be unique across
all :class:`.Operation` objects in the API. all :class:`.Operation` objects in the API.
@@ -308,6 +311,8 @@ class SwaggerAutoSchema(ViewInspector):
of this view in the API; e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc. of this view in the API; e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc.
:rtype: str :rtype: str
""" """
operation_keys = operation_keys or self.operation_keys
operation_id = self.overrides.get('operation_id', '') operation_id = self.overrides.get('operation_id', '')
if not operation_id: if not operation_id:
operation_id = '_'.join(operation_keys) operation_id = '_'.join(operation_keys)
@@ -369,7 +374,7 @@ class SwaggerAutoSchema(ViewInspector):
""" """
return self.overrides.get('deprecated', None) return self.overrides.get('deprecated', None)
def get_tags(self, operation_keys): def get_tags(self, operation_keys=None):
"""Get a list of tags for this operation. Tags determine how operations relate with each other, and in the UI """Get a list of tags for this operation. Tags determine how operations relate with each other, and in the UI
each tag will show as a group containing the operations that use it. If not provided in overrides, each tag will show as a group containing the operations that use it. If not provided in overrides,
tags will be inferred from the operation url. tags will be inferred from the operation url.
@@ -378,6 +383,8 @@ class SwaggerAutoSchema(ViewInspector):
of this view in the API; e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc. of this view in the API; e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc.
:rtype: list[str] :rtype: list[str]
""" """
operation_keys = operation_keys or self.operation_keys
tags = self.overrides.get('tags') tags = self.overrides.get('tags')
if not tags: if not tags:
tags = [operation_keys[0]] tags = [operation_keys[0]]
+4 -4
View File
@@ -166,7 +166,7 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
if len(available_http_methods) > 1: if len(available_http_methods) > 1:
assert _methods, \ assert _methods, \
"on multi-method api_view, action, detail_route or list_route, you must specify " \ "on multi-method api_view or action, you must specify " \
"swagger_auto_schema on a per-method basis using one of the `method` or `methods` arguments" "swagger_auto_schema on a per-method basis using one of the `method` or `methods` arguments"
else: else:
# for a single-method view we assume that single method as the decorator target # for a single-method view we assume that single method as the decorator target
@@ -179,8 +179,8 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
view_method._swagger_auto_schema = existing_data view_method._swagger_auto_schema = existing_data
else: else:
assert not _methods, \ assert not _methods, \
"the methods argument should only be specified when decorating an action, detail_route or " \ "the methods argument should only be specified when decorating an action; " \
"list_route; you should also ensure that you put the swagger_auto_schema decorator " \ "you should also ensure that you put the swagger_auto_schema decorator " \
"AFTER (above) the _route decorator" "AFTER (above) the _route decorator"
assert not existing_data, "swagger_auto_schema applied twice to method" assert not existing_data, "swagger_auto_schema applied twice to method"
view_method._swagger_auto_schema = data view_method._swagger_auto_schema = data
@@ -215,7 +215,7 @@ def is_list_view(path, method, view):
:param APIView view: target view :param APIView view: target view
:rtype: bool :rtype: bool
""" """
# for ViewSets, it could be the default 'list' action, or a list_route # for ViewSets, it could be the default 'list' action, or an @action(detail=False)
action = getattr(view, 'action', '') action = getattr(view, 'action', '')
method = getattr(view, action, None) or method method = getattr(view, action, None) or method
detail = getattr(method, 'detail', None) detail = getattr(method, 'detail', None)
+3 -12
View File
@@ -1,10 +1,8 @@
import datetime import datetime
import functools
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets from rest_framework import viewsets
# noinspection PyDeprecation
from rest_framework.filters import OrderingFilter from rest_framework.filters import OrderingFilter
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from rest_framework.parsers import FileUploadParser, MultiPartParser from rest_framework.parsers import FileUploadParser, MultiPartParser
@@ -93,17 +91,10 @@ class ArticleViewSet(viewsets.ModelViewSet):
swagger_schema = NoTitleAutoSchema swagger_schema = NoTitleAutoSchema
try: from rest_framework.decorators import action
from rest_framework.decorators import action
list_route = functools.partial(action, detail=False)
detail_route = functools.partial(action, detail=True)
except ImportError:
# TODO: remove when dropping support for DRF 3.7
action = None
from rest_framework.decorators import list_route, detail_route
@swagger_auto_schema(auto_schema=NoPagingAutoSchema, filter_inspectors=[DjangoFilterDescriptionInspector]) @swagger_auto_schema(auto_schema=NoPagingAutoSchema, filter_inspectors=[DjangoFilterDescriptionInspector])
@list_route(methods=['get']) @action(detail=False, methods=['get'])
def today(self, request): def today(self, request):
today_min = datetime.datetime.combine(datetime.date.today(), datetime.time.min) today_min = datetime.datetime.combine(datetime.date.today(), datetime.time.min)
today_max = datetime.datetime.combine(datetime.date.today(), datetime.time.max) today_max = datetime.datetime.combine(datetime.date.today(), datetime.time.max)
@@ -118,7 +109,7 @@ class ArticleViewSet(viewsets.ModelViewSet):
type=openapi.TYPE_INTEGER, type=openapi.TYPE_INTEGER,
description="this should not crash (form parameter on DELETE method)" description="this should not crash (form parameter on DELETE method)"
)]) )])
@detail_route(methods=['get', 'post', 'delete'], parser_classes=(MultiPartParser, FileUploadParser)) @action(detail=True, methods=['get', 'post', 'delete'], parser_classes=(MultiPartParser, FileUploadParser))
def image(self, request, slug=None): def image(self, request, slug=None):
""" """
image method docstring image method docstring
+6
View File
@@ -1,13 +1,19 @@
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.pagination import BasePagination
from .models import Identity, Person from .models import Identity, Person
from .serializers import IdentitySerializer, PersonSerializer from .serializers import IdentitySerializer, PersonSerializer
class UnknownPagination(BasePagination):
paginator_query_args = ['unknown_paginator']
class PersonViewSet(viewsets.ModelViewSet): class PersonViewSet(viewsets.ModelViewSet):
model = Person model = Person
queryset = Person.objects queryset = Person.objects
serializer_class = PersonSerializer serializer_class = PersonSerializer
pagination_class = UnknownPagination
class IdentityViewSet(viewsets.ModelViewSet): class IdentityViewSet(viewsets.ModelViewSet):
+6 -1
View File
@@ -1,8 +1,13 @@
import rest_framework
from decimal import Decimal from decimal import Decimal
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from packaging.version import Version
if Version(rest_framework.__version__) < Version('3.10'):
from rest_framework.compat import MaxLengthValidator, MinValueValidator
else:
from django.core.validators import MaxLengthValidator, MinValueValidator
from rest_framework import serializers from rest_framework import serializers
from rest_framework.compat import MaxLengthValidator, MinValueValidator
from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet, SnippetViewer from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet, SnippetViewer
+11
View File
@@ -0,0 +1,11 @@
from drf_yasg import openapi
from drf_yasg.inspectors import NotHandled, PaginatorInspector
class UnknownPaginatorInspector(PaginatorInspector):
def get_paginator_parameters(self, paginator):
if hasattr(paginator, 'paginator_query_args'):
return [openapi.Parameter(name=arg, in_=openapi.IN_QUERY, type=openapi.TYPE_STRING)
for arg in getattr(paginator, 'paginator_query_args')]
return NotHandled
+6 -1
View File
@@ -140,7 +140,12 @@ SWAGGER_SETTINGS = {
'clientId': OAUTH2_CLIENT_ID, 'clientId': OAUTH2_CLIENT_ID,
'clientSecret': OAUTH2_CLIENT_SECRET, 'clientSecret': OAUTH2_CLIENT_SECRET,
'appName': OAUTH2_APP_NAME, 'appName': OAUTH2_APP_NAME,
} },
"DEFAULT_PAGINATOR_INSPECTORS": [
'testproj.inspectors.UnknownPaginatorInspector',
'drf_yasg.inspectors.DjangoRestResponsePagination',
'drf_yasg.inspectors.CoreAPICompatInspector',
]
} }
REDOC_SETTINGS = { REDOC_SETTINGS = {
+4 -1
View File
@@ -276,7 +276,10 @@ paths:
get: get:
operationId: people_list operationId: people_list
description: '' description: ''
parameters: [] parameters:
- name: unknown_paginator
in: query
type: string
responses: responses:
'200': '200':
description: '' description: ''
+43
View File
@@ -3,11 +3,14 @@ from collections import OrderedDict
import pytest import pytest
from django.conf.urls import url from django.conf.urls import url
from django.contrib.postgres import fields as postgres_fields
from django.db import models
from django.utils.inspect import get_func_args from django.utils.inspect import get_func_args
from rest_framework import routers, serializers, viewsets from rest_framework import routers, serializers, viewsets
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from django_fake_model import models as fake_models
from drf_yasg import codecs, openapi from drf_yasg import codecs, openapi
from drf_yasg.codecs import yaml_sane_load from drf_yasg.codecs import yaml_sane_load
from drf_yasg.errors import SwaggerGenerationError from drf_yasg.errors import SwaggerGenerationError
@@ -230,3 +233,43 @@ def test_choice_field(choices, expected_type):
property_schema = swagger['definitions']['Detail']['properties']['detail'] property_schema = swagger['definitions']['Detail']['properties']['detail']
assert property_schema == openapi.Schema(title='Detail', type=expected_type, enum=choices) assert property_schema == openapi.Schema(title='Detail', type=expected_type, enum=choices)
@pytest.mark.parametrize('choices, field, expected_type', [
([1, 2, 3], models.IntegerField, openapi.TYPE_INTEGER),
(["A", "B"], models.CharField, openapi.TYPE_STRING),
])
def test_nested_choice_in_array_field(choices, field, expected_type):
# Create a model class on the fly to avoid warnings about using the several
# model class name several times
model_class = type(
"%sModel" % field.__name__,
(fake_models.FakeModel,),
{
"array": postgres_fields.ArrayField(
field(choices=((i, "choice %s" % i) for i in choices))
),
"__module__": "test_models",
}
)
class ArraySerializer(serializers.ModelSerializer):
class Meta:
model = model_class
fields = ("array",)
class ArrayViewSet(viewsets.ModelViewSet):
serializer_class = ArraySerializer
router = routers.DefaultRouter()
router.register(r'arrays', ArrayViewSet, **_basename_or_base_name('arrays'))
generator = OpenAPISchemaGenerator(
info=openapi.Info(title='Test array model generator', default_version='v1'),
patterns=router.urls
)
swagger = generator.get_schema(None, True)
property_schema = swagger['definitions']['Array']['properties']['array']['items']
assert property_schema == openapi.Schema(title='Array', type=expected_type, enum=choices)
+6 -5
View File
@@ -6,8 +6,10 @@ isolated_build_env = .package
# https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django # https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django
envlist = envlist =
py27-django111-drf39-typing, py27-django111-drf39-typing,
py{27,35,36}-django111-drf{37,38,39}, py27-django111-drf{38,39},
py{35,36,37}-django{20,21,22}-drf{37,38,39}, py{35,36}-django{111,21,22}-drf{38,39},
py37-django{21,22}-drf{38,39},
py37-django{21,22}-drf310,
djmaster, lint, docs djmaster, lint, docs
[testenv:.package] [testenv:.package]
@@ -17,13 +19,12 @@ deps =
[testenv] [testenv]
deps = deps =
django111: Django>=1.11,<2.0 django111: Django>=1.11,<2.0
django20: Django>=2.0,<2.1
django21: Django>=2.1,<2.2 django21: Django>=2.1,<2.2
django22: Django>=2.2,<2.3 django22: Django>=2.2,<2.3
drf37: djangorestframework>=3.7.7,<3.8 drf38: djangorestframework>=3.8,<3.9
drf38: djangorestframework>=3.8.0,<3.9
drf39: djangorestframework>=3.9,<3.10 drf39: djangorestframework>=3.9,<3.10
drf310: djangorestframework>=3.10
typing: typing>=3.6.6 typing: typing>=3.6.6