Clean up Django 2 path backslashes
In Django 2, routes defines via urls.path are aggresively escaped when converted into regex. This is a naive fix which unescapes all characters outside capture groups, but in the context of OpenAPI is okay because regular expressions inside paths are not supported anyway. This issue affects django-rest-framework as well, as outlined in encode/django-rest-framework#5672, encode/django-rest-framework#5675.openapi3
parent
f6c30181fe
commit
521172c195
|
|
@ -8,6 +8,8 @@ Changelog
|
||||||
*********
|
*********
|
||||||
|
|
||||||
- **FIX:** fixed a crash caused by having read-only Serializers nested by reference
|
- **FIX:** fixed a crash caused by having read-only Serializers nested by reference
|
||||||
|
- **FIX:** removed erroneous backslashes in paths when routes are generated using Django 2
|
||||||
|
`path() <https://docs.djangoproject.com/en/2.0/ref/urls/#django.urls.path>`_
|
||||||
- **IMPROVEMENT:** updated ``swagger-ui`` to version 3.7.0
|
- **IMPROVEMENT:** updated ``swagger-ui`` to version 3.7.0
|
||||||
|
|
||||||
*********
|
*********
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,7 @@ nitpick_ignore = [
|
||||||
|
|
||||||
('py:class', 'ruamel.yaml.dumper.SafeDumper'),
|
('py:class', 'ruamel.yaml.dumper.SafeDumper'),
|
||||||
('py:class', 'rest_framework.renderers.BaseRenderer'),
|
('py:class', 'rest_framework.renderers.BaseRenderer'),
|
||||||
|
('py:class', 'rest_framework.schemas.generators.EndpointEnumerator'),
|
||||||
('py:class', 'rest_framework.views.APIView'),
|
('py:class', 'rest_framework.views.APIView'),
|
||||||
|
|
||||||
('py:class', 'OpenAPICodecYaml'),
|
('py:class', 'OpenAPICodecYaml'),
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,63 @@
|
||||||
|
import re
|
||||||
from collections import defaultdict, OrderedDict
|
from collections import defaultdict, OrderedDict
|
||||||
|
|
||||||
import django.db.models
|
import django.db.models
|
||||||
import uritemplate
|
import uritemplate
|
||||||
from coreapi.compat import force_text
|
from coreapi.compat import force_text
|
||||||
from rest_framework.schemas.generators import SchemaGenerator
|
from rest_framework.schemas.generators import SchemaGenerator, EndpointEnumerator as _EndpointEnumerator
|
||||||
from rest_framework.schemas.inspectors import get_pk_description
|
from rest_framework.schemas.inspectors import get_pk_description
|
||||||
|
|
||||||
from . import openapi
|
from . import openapi
|
||||||
from .inspectors import SwaggerAutoSchema
|
from .inspectors import SwaggerAutoSchema
|
||||||
from .openapi import ReferenceResolver
|
from .openapi import ReferenceResolver
|
||||||
|
|
||||||
|
PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}')
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointEnumerator(_EndpointEnumerator):
|
||||||
|
def get_path_from_regex(self, path_regex):
|
||||||
|
return self.unescape_path(super(EndpointEnumerator, self).get_path_from_regex(path_regex))
|
||||||
|
|
||||||
|
def unescape(self, s):
|
||||||
|
"""Unescape all backslash escapes from `s`.
|
||||||
|
|
||||||
|
:param str s: string with backslash escapes
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
# unlike .replace('\\', ''), this corectly transforms a double backslash into a single backslash
|
||||||
|
return re.sub(r'\\(.)', r'\1', s)
|
||||||
|
|
||||||
|
def unescape_path(self, path):
|
||||||
|
"""Remove backslashes from all path components outside {parameters}. This is needed because
|
||||||
|
Django>=2.0 ``path()``/``RoutePattern`` aggresively escapes all non-parameter path components.
|
||||||
|
|
||||||
|
**NOTE:** this might destructively affect some url regex patterns that contain metacharacters (e.g. \w, \d)
|
||||||
|
outside path parameter groups; if you are in this category, God help you
|
||||||
|
|
||||||
|
:param str path: path possibly containing
|
||||||
|
:return: the unescaped path
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
original_path = path
|
||||||
|
clean_path = ''
|
||||||
|
while path:
|
||||||
|
match = PATH_PARAMETER_RE.search(path)
|
||||||
|
if not match:
|
||||||
|
clean_path += self.unescape(path)
|
||||||
|
break
|
||||||
|
clean_path += self.unescape(path[:match.start()])
|
||||||
|
clean_path += match.group()
|
||||||
|
path = path[match.end():]
|
||||||
|
|
||||||
|
return clean_path
|
||||||
|
|
||||||
|
|
||||||
class OpenAPISchemaGenerator(object):
|
class OpenAPISchemaGenerator(object):
|
||||||
"""
|
"""
|
||||||
This class iterates over all registered API endpoints and returns an appropriate OpenAPI 2.0 compliant schema.
|
This class iterates over all registered API endpoints and returns an appropriate OpenAPI 2.0 compliant schema.
|
||||||
Method implementations shamelessly stolen and adapted from rest_framework SchemaGenerator.
|
Method implementations shamelessly stolen and adapted from rest_framework SchemaGenerator.
|
||||||
"""
|
"""
|
||||||
|
endpoint_enumerator_class = EndpointEnumerator
|
||||||
|
|
||||||
def __init__(self, info, version, url=None, patterns=None, urlconf=None):
|
def __init__(self, info, version, url=None, patterns=None, urlconf=None):
|
||||||
"""
|
"""
|
||||||
|
|
@ -79,8 +121,8 @@ class OpenAPISchemaGenerator(object):
|
||||||
:return: {path: (view_class, list[(http_method, view_instance)])
|
:return: {path: (view_class, list[(http_method, view_instance)])
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
inspector = self._gen.endpoint_inspector_cls(self._gen.patterns, self._gen.urlconf)
|
enumerator = self.endpoint_enumerator_class(self._gen.patterns, self._gen.urlconf)
|
||||||
endpoints = inspector.get_api_endpoints()
|
endpoints = enumerator.get_api_endpoints()
|
||||||
|
|
||||||
view_paths = defaultdict(list)
|
view_paths = defaultdict(list)
|
||||||
view_cls = {}
|
view_cls = {}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,17 @@
|
||||||
from django.conf.urls import url
|
import django
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
if django.VERSION[:2] >= (2, 0):
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'$', views.SnippetList.as_view()),
|
path('', views.SnippetList.as_view()),
|
||||||
url(r'^(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view()),
|
path('<int:pk>/', views.SnippetDetail.as_view()),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
from django.conf.urls import url
|
||||||
|
urlpatterns = [
|
||||||
|
url('^$', views.SnippetList.as_view()),
|
||||||
|
url(r'^(?P<pk>\d+)/$', views.SnippetDetail.as_view()),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,5 @@ from users import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', views.UserList.as_view()),
|
url(r'^$', views.UserList.as_view()),
|
||||||
url(r'^(?P<pk>[0-9]+)/$', views.user_detail),
|
url(r'^(?P<pk>\d+)/$', views.user_detail),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue