Skip to content

Commit

Permalink
Clean up Django 2 path backslashes
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
axnsan12 committed Dec 18, 2017
1 parent f6c3018 commit 521172c
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 9 deletions.
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Changelog
*********

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

*********
Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@

('py:class', 'ruamel.yaml.dumper.SafeDumper'),
('py:class', 'rest_framework.renderers.BaseRenderer'),
('py:class', 'rest_framework.schemas.generators.EndpointEnumerator'),
('py:class', 'rest_framework.views.APIView'),

('py:class', 'OpenAPICodecYaml'),
Expand Down
48 changes: 45 additions & 3 deletions src/drf_yasg/generators.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,63 @@
import re
from collections import defaultdict, OrderedDict

import django.db.models
import uritemplate
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 . import openapi
from .inspectors import SwaggerAutoSchema
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):
"""
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.
"""
endpoint_enumerator_class = EndpointEnumerator

def __init__(self, info, version, url=None, patterns=None, urlconf=None):
"""
Expand Down Expand Up @@ -79,8 +121,8 @@ def get_endpoints(self, request=None):
:return: {path: (view_class, list[(http_method, view_instance)])
:rtype: dict
"""
inspector = self._gen.endpoint_inspector_cls(self._gen.patterns, self._gen.urlconf)
endpoints = inspector.get_api_endpoints()
enumerator = self.endpoint_enumerator_class(self._gen.patterns, self._gen.urlconf)
endpoints = enumerator.get_api_endpoints()

view_paths = defaultdict(list)
view_cls = {}
Expand Down
19 changes: 14 additions & 5 deletions testproj/snippets/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
from django.conf.urls import url
import django

from . import views

urlpatterns = [
url(r'$', views.SnippetList.as_view()),
url(r'^(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view()),
]
if django.VERSION[:2] >= (2, 0):
from django.urls import path

urlpatterns = [
path('', views.SnippetList.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()),
]
2 changes: 1 addition & 1 deletion testproj/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@

urlpatterns = [
url(r'^$', views.UserList.as_view()),
url(r'^(?P<pk>[0-9]+)/$', views.user_detail),
url(r'^(?P<pk>\d+)/$', views.user_detail),
]

0 comments on commit 521172c

Please sign in to comment.