Skip to content

Commit d138f30

Browse files
author
Carlton Gibson
authored
Fix naming collisions in Schema Generation (#5464)
* Add failing tests for #4704 * Add generic view based test case. * Adjust insert_into to raise ValueError
1 parent 2befa6c commit d138f30

File tree

2 files changed

+107
-4
lines changed

2 files changed

+107
-4
lines changed

rest_framework/schemas/generators.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@ def is_api_view(callback):
5151
return (cls is not None) and issubclass(cls, APIView)
5252

5353

54+
INSERT_INTO_COLLISION_FMT = """
55+
Schema Naming Collision.
56+
57+
coreapi.Link for URL path {value_url} cannot be inserted into schema.
58+
Position conflicts with coreapi.Link for URL path {target_url}.
59+
60+
Attemped to insert link with keys: {keys}.
61+
62+
Adjust URLs to avoid naming collision or override `SchemaGenerator.get_keys()`
63+
to customise schema structure.
64+
"""
65+
66+
5467
def insert_into(target, keys, value):
5568
"""
5669
Nested dictionary insertion.
@@ -64,7 +77,15 @@ def insert_into(target, keys, value):
6477
if key not in target:
6578
target[key] = {}
6679
target = target[key]
67-
target[keys[-1]] = value
80+
try:
81+
target[keys[-1]] = value
82+
except TypeError:
83+
msg = INSERT_INTO_COLLISION_FMT.format(
84+
value_url=value.url,
85+
target_url=target.url,
86+
keys=keys
87+
)
88+
raise ValueError(msg)
6889

6990

7091
def is_custom_action(action):

tests/test_schemas.py

+85-3
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,25 @@
66
from django.http import Http404
77
from django.test import TestCase, override_settings
88

9-
from rest_framework import filters, pagination, permissions, serializers
9+
from rest_framework import (
10+
filters, generics, pagination, permissions, serializers
11+
)
1012
from rest_framework.compat import coreapi, coreschema
1113
from rest_framework.decorators import (
1214
api_view, detail_route, list_route, schema
1315
)
1416
from rest_framework.request import Request
15-
from rest_framework.routers import DefaultRouter
17+
from rest_framework.routers import DefaultRouter, SimpleRouter
1618
from rest_framework.schemas import (
1719
AutoSchema, ManualSchema, SchemaGenerator, get_schema_view
1820
)
1921
from rest_framework.schemas.generators import EndpointEnumerator
2022
from rest_framework.test import APIClient, APIRequestFactory
2123
from rest_framework.utils import formatting
2224
from rest_framework.views import APIView
23-
from rest_framework.viewsets import ModelViewSet
25+
from rest_framework.viewsets import GenericViewSet, ModelViewSet
26+
27+
from .models import BasicModel
2428

2529
factory = APIRequestFactory()
2630

@@ -726,3 +730,81 @@ def get(self, request, *args, **kwargs):
726730
"The `OldFashionedExcludedView.exclude_from_schema` attribute is "
727731
"pending deprecation. Set `schema = None` instead."
728732
)
733+
734+
735+
@api_view(["GET"])
736+
def simple_fbv(request):
737+
pass
738+
739+
740+
class BasicModelSerializer(serializers.ModelSerializer):
741+
class Meta:
742+
model = BasicModel
743+
fields = "__all__"
744+
745+
746+
class NamingCollisionView(generics.RetrieveUpdateDestroyAPIView):
747+
queryset = BasicModel.objects.all()
748+
serializer_class = BasicModelSerializer
749+
750+
751+
class NamingCollisionViewSet(GenericViewSet):
752+
"""
753+
Example via: https://stackoverflow.com/questions/43778668/django-rest-framwork-occured-typeerror-link-object-does-not-support-item-ass/
754+
"""
755+
permision_class = ()
756+
757+
@list_route()
758+
def detail(self, request):
759+
return {}
760+
761+
@list_route(url_path='detail/export')
762+
def detail_export(self, request):
763+
return {}
764+
765+
766+
naming_collisions_router = SimpleRouter()
767+
naming_collisions_router.register(r'collision', NamingCollisionViewSet, base_name="collision")
768+
769+
770+
class TestURLNamingCollisions(TestCase):
771+
"""
772+
Ref: https://github.com/encode/django-rest-framework/issues/4704
773+
"""
774+
def test_manually_routing_nested_routes(self):
775+
patterns = [
776+
url(r'^test', simple_fbv),
777+
url(r'^test/list/', simple_fbv),
778+
]
779+
780+
generator = SchemaGenerator(title='Naming Colisions', patterns=patterns)
781+
782+
with pytest.raises(ValueError):
783+
generator.get_schema()
784+
785+
def test_manually_routing_generic_view(self):
786+
patterns = [
787+
url(r'^test', NamingCollisionView.as_view()),
788+
url(r'^test/retrieve/', NamingCollisionView.as_view()),
789+
url(r'^test/update/', NamingCollisionView.as_view()),
790+
791+
# Fails with method names:
792+
url(r'^test/get/', NamingCollisionView.as_view()),
793+
url(r'^test/put/', NamingCollisionView.as_view()),
794+
url(r'^test/delete/', NamingCollisionView.as_view()),
795+
]
796+
797+
generator = SchemaGenerator(title='Naming Colisions', patterns=patterns)
798+
799+
with pytest.raises(ValueError):
800+
generator.get_schema()
801+
802+
def test_from_router(self):
803+
patterns = [
804+
url(r'from-router', include(naming_collisions_router.urls)),
805+
]
806+
807+
generator = SchemaGenerator(title='Naming Colisions', patterns=patterns)
808+
809+
with pytest.raises(ValueError):
810+
generator.get_schema()

0 commit comments

Comments
 (0)