diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 5009ffee19..9502c245f5 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -32,7 +32,7 @@ def get_regex_pattern(urlpattern): if hasattr(urlpattern, 'pattern'): # Django 2.0 - return urlpattern.pattern.regex.pattern + return str(urlpattern.pattern) else: # Django < 2.0 return urlpattern.regex.pattern @@ -255,6 +255,14 @@ def md_filter_add_syntax_highlight(md): except ImportError: 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 # See: http://bugs.python.org/issue22767 diff --git a/rest_framework/schemas/generators.py b/rest_framework/schemas/generators.py index 2fe4927d83..6f5c044756 100644 --- a/rest_framework/schemas/generators.py +++ b/rest_framework/schemas/generators.py @@ -3,6 +3,7 @@ See schemas.__init__.py for package overview. """ +import re import warnings from collections import Counter, OrderedDict from importlib import import_module @@ -135,6 +136,11 @@ def endpoint_ordering(endpoint): return (path, method_priority) +_PATH_PARAMETER_COMPONENT_RE = re.compile( + r'<(?:(?P[^>:]+):)?(?P\w+)>' +) + + class EndpointEnumerator(object): """ A class to determine the available API endpoints that a project exposes. @@ -189,7 +195,9 @@ def get_path_from_regex(self, path_regex): Given a URL conf regex, return a URI template string. """ 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}', path) return path def should_include_endpoint(self, path, callback): diff --git a/tests/test_schemas.py b/tests/test_schemas.py index fa91bac038..34cb20798a 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -9,7 +9,7 @@ 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, get_regex_pattern, path from rest_framework.decorators import ( api_view, detail_route, list_route, schema ) @@ -361,6 +361,59 @@ def test_schema_for_regular_views(self): 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//', ExampleDetailView.as_view()), + path('example//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') class TestSchemaGeneratorNotAtRoot(TestCase): def setUp(self):