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
Cristi Vîjdea 2017-12-18 00:58:02 +01:00
parent f6c30181fe
commit 521172c195
5 changed files with 63 additions and 9 deletions

View File

@ -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
********* *********

View File

@ -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'),

View File

@ -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 = {}

View File

@ -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()),
] ]

View File

@ -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),
] ]