Skip to content

Commit

Permalink
bugfix parameterized patterns for namespace versioning #145
Browse files Browse the repository at this point in the history
  • Loading branch information
tfranzel committed Sep 6, 2020
1 parent 8179d07 commit ea4c569
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 3 deletions.
51 changes: 49 additions & 2 deletions drf_spectacular/plumbing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import hashlib
import inspect
import json
import re
import sys
from abc import ABCMeta
from collections import OrderedDict, defaultdict
Expand All @@ -12,7 +13,10 @@
import uritemplate
from django import __version__ as DJANGO_VERSION
from django.apps import apps
from django.urls.resolvers import _PATH_PARAMETER_COMPONENT_RE, get_resolver # type: ignore
from django.urls.resolvers import ( # type: ignore
_PATH_PARAMETER_COMPONENT_RE, RegexPattern, Resolver404, RoutePattern, URLPattern, URLResolver,
get_resolver,
)
from django.utils.functional import Promise
from django.utils.module_loading import import_string
from rest_framework import exceptions, fields, mixins, serializers, versioning
Expand Down Expand Up @@ -636,14 +640,57 @@ def modify_for_versioning(patterns, method, path, view, requested_version):
# emulate router behaviour by injecting substituted variable into view
view.kwargs[version_param] = requested_version
elif issubclass(view.versioning_class, versioning.NamespaceVersioning):
request.resolver_match = get_resolver(tuple(patterns)).resolve(path)
try:
request.resolver_match = get_resolver(
urlconf=tuple(detype_pattern(p) for p in patterns)
).resolve(path)
except Resolver404:
error(f"namespace versioning path resolution failed for {path}. path will be ignored.")
elif issubclass(view.versioning_class, versioning.AcceptHeaderVersioning):
neg = view.perform_content_negotiation(view.request)
view.request.accepted_renderer, view.request.accepted_media_type = neg

return path


def detype_pattern(pattern):
"""
return an equivalent pattern that accepts arbitrary values for path parameters.
de-typing the path will ease determining a matching route without having properly
formatted dummy values for all path parameters.
"""
if isinstance(pattern, URLResolver):
return URLResolver(
pattern=detype_pattern(pattern.pattern),
urlconf_name=[detype_pattern(p) for p in pattern.urlconf_name],
default_kwargs=pattern.default_kwargs,
app_name=pattern.app_name,
namespace=pattern.namespace,
)
elif isinstance(pattern, URLPattern):
return URLPattern(
pattern=detype_pattern(pattern.pattern),
callback=pattern.callback,
default_args=pattern.default_args,
name=pattern.name,
)
elif isinstance(pattern, RoutePattern):
return RoutePattern(
route=re.sub(r'<\w+:(\w+)>', r'<\1>', pattern._route),
name=pattern.name,
is_endpoint=pattern._is_endpoint
)
elif isinstance(pattern, RegexPattern):
return RegexPattern(
regex=re.sub(r'\(\?P<(\w+)>.*\)', r'(?P<\1>[^/]+)', pattern._regex),
name=pattern.name,
is_endpoint=pattern._is_endpoint
)
else:
warn(f'unexpected pattern "{pattern}" encountered while simplifying urlpatterns.')
return pattern


def normalize_result_object(result):
""" resolve non-serializable objects like lazy translation strings and OrderedDict """
if isinstance(result, dict) or isinstance(result, OrderedDict):
Expand Down
26 changes: 25 additions & 1 deletion tests/test_versioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.conf.urls import include
from django.db import models
from django.urls import path, re_path
from rest_framework import mixins, routers, serializers, viewsets
from rest_framework import generics, mixins, routers, serializers, viewsets
from rest_framework.test import APIClient
from rest_framework.versioning import AcceptHeaderVersioning, NamespaceVersioning, URLPathVersioning

Expand Down Expand Up @@ -102,6 +102,30 @@ def test_namespace_versioning(no_warnings, viewset_cls, version):
assert_schema(schema, f'tests/test_versioning_{version}.yml')


def test_namespace_versioning_urlpatterns_simplification(no_warnings):
class NamespaceVersioningAPIView(generics.RetrieveUpdateDestroyAPIView):
versioning_class = NamespaceVersioning
serializer_class = Xv1Serializer
queryset = VersioningModel.objects.all()

urls = (
path('x/<int:pk>/', NamespaceVersioningAPIView.as_view()),
path('y/<pk>/', NamespaceVersioningAPIView.as_view()),
re_path('z/(?P<pk>[0-9A-Fa-f-]+)/', NamespaceVersioningAPIView.as_view()),
)
generator = SchemaGenerator(
patterns=[path('v1/<int:some_param>/', include((urls, 'v1'))), ],
api_version='v1',
)
schema = generator.get_schema(request=None, public=True)

for s in ['x', 'y', 'z']:
parameters = schema['paths'][f'/v1/{{some_param}}/{s}/{{id}}/']['get']['parameters']
parameters = {p['name']: p for p in parameters}
assert parameters['id']['schema']['type'] == 'integer'
assert parameters['some_param']['schema']['type'] == 'integer'


@pytest.mark.parametrize('viewset_cls', [AcceptHeaderVersioningViewset, AcceptHeaderVersioningViewset2])
@pytest.mark.parametrize('version', ['v1', 'v2'])
def test_accept_header_versioning(no_warnings, viewset_cls, version):
Expand Down

0 comments on commit ea4c569

Please sign in to comment.