Fix url parsing in schema generation

- Call `str(pattern)` to get non-escaped route
- Strip converters from path to comply with uritemplate format

Fixes #5675
This commit is contained in:
Tilmann Becker 2017-12-19 20:26:20 +01:00
parent ea0b3b32ad
commit 0dc31b23d8
4 changed files with 75 additions and 4 deletions

View File

@ -32,7 +32,7 @@ except ImportError:
def get_regex_pattern(urlpattern): def get_regex_pattern(urlpattern):
if hasattr(urlpattern, 'pattern'): if hasattr(urlpattern, 'pattern'):
# Django 2.0 # Django 2.0
return urlpattern.pattern.regex.pattern return str(urlpattern.pattern)
else: else:
# Django < 2.0 # Django < 2.0
return urlpattern.regex.pattern return urlpattern.regex.pattern
@ -255,6 +255,14 @@ try:
except ImportError: except ImportError:
InvalidTimeError = Exception InvalidTimeError = Exception
# Django 1.x url routing syntax. Remove when dropping Django 1.11 support.
try:
from django.urls import include, path, re_path # noqa
except ImportError:
from django.conf.urls import include, url # noqa
path = 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: http://bugs.python.org/issue22767 # See: http://bugs.python.org/issue22767

View File

@ -3,6 +3,7 @@ generators.py # Top-down schema generation
See schemas.__init__.py for package overview. See schemas.__init__.py for package overview.
""" """
import re
import warnings import warnings
from collections import Counter, OrderedDict from collections import Counter, OrderedDict
from importlib import import_module from importlib import import_module
@ -135,6 +136,11 @@ def endpoint_ordering(endpoint):
return (path, method_priority) return (path, method_priority)
_PATH_PARAMETER_COMPONENT_RE = re.compile(
r'<(?:(?P<converter>[^>:]+):)?(?P<parameter>\w+)>'
)
class EndpointEnumerator(object): class EndpointEnumerator(object):
""" """
A class to determine the available API endpoints that a project exposes. A class to determine the available API endpoints that a project exposes.
@ -189,7 +195,9 @@ class EndpointEnumerator(object):
Given a URL conf regex, return a URI template string. Given a URL conf regex, return a URI template string.
""" """
path = simplify_regex(path_regex) path = simplify_regex(path_regex)
path = path.replace('<', '{').replace('>', '}')
# Strip Django 2.0 convertors as they are incompatible with uritemplate format
path = re.sub(_PATH_PARAMETER_COMPONENT_RE, r'{\g<parameter>}', path)
return path return path
def should_include_endpoint(self, path, callback): def should_include_endpoint(self, path, callback):

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
import re import re
from collections import MutableMapping, OrderedDict from collections import MutableMapping, OrderedDict
import coreapi
import pytest import pytest
from django.conf.urls import include, url from django.conf.urls import include, url
from django.core.cache import cache from django.core.cache import cache
@ -14,7 +15,6 @@ from django.utils import six
from django.utils.safestring import SafeText from django.utils.safestring import SafeText
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import coreapi
from rest_framework import permissions, serializers, status from rest_framework import permissions, serializers, status
from rest_framework.renderers import ( from rest_framework.renderers import (
AdminRenderer, BaseRenderer, BrowsableAPIRenderer, DocumentationRenderer, AdminRenderer, BaseRenderer, BrowsableAPIRenderer, DocumentationRenderer,

View File

@ -1,6 +1,7 @@
import unittest import unittest
import pytest import pytest
from django.conf.urls import include, url 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
@ -9,7 +10,7 @@ from django.test import TestCase, override_settings
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 from rest_framework.compat import coreapi, coreschema, get_regex_pattern, path
from rest_framework.decorators import ( from rest_framework.decorators import (
api_view, detail_route, list_route, schema api_view, detail_route, list_route, schema
) )
@ -27,6 +28,7 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet
from .models import BasicModel from .models import BasicModel
factory = APIRequestFactory() factory = APIRequestFactory()
@ -361,6 +363,59 @@ class TestSchemaGenerator(TestCase):
assert schema == expected assert schema == expected
@unittest.skipUnless(coreapi, 'coreapi is not installed')
@unittest.skipUnless(path, 'needs Django 2')
class TestSchemaGeneratorDjango2(TestCase):
def setUp(self):
self.patterns = [
path('example/', ExampleListView.as_view()),
path('example/<int:pk>/', ExampleDetailView.as_view()),
path('example/<int:pk>/sub/', ExampleDetailView.as_view()),
]
def test_schema_for_regular_views(self):
"""
Ensure that schema generation works for APIView classes.
"""
generator = SchemaGenerator(title='Example API', patterns=self.patterns)
schema = generator.get_schema()
expected = coreapi.Document(
url='',
title='Example API',
content={
'example': {
'create': coreapi.Link(
url='/example/',
action='post',
fields=[]
),
'list': coreapi.Link(
url='/example/',
action='get',
fields=[]
),
'read': coreapi.Link(
url='/example/{id}/',
action='get',
fields=[
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
]
),
'sub': {
'list': coreapi.Link(
url='/example/{id}/sub/',
action='get',
fields=[
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
]
)
}
}
}
)
assert schema == expected
@unittest.skipUnless(coreapi, 'coreapi is not installed') @unittest.skipUnless(coreapi, 'coreapi is not installed')
class TestSchemaGeneratorNotAtRoot(TestCase): class TestSchemaGeneratorNotAtRoot(TestCase):
def setUp(self): def setUp(self):