Repository: encode/django-rest-framework

Issue:
Schema generation doesn't work if URL ends with method name
## Steps to reproduce

If one would use urls like these (not a good practice but possible):

    url(r'^test', ExampleView1.as_view()),
    url(r'^test/delete/', ExampleView2.as_view()),

Schema generation using rest_framework.schemas.get_schema_view will fail.
I've attached minimal [test django project](https://github.com/tomchristie/django-rest-framework/files/611291/schema_bug_test.tar.gz) if anyone would like to reproduce the issue.


## Expected behavior

Schema should be generated as normally does.

## Actual behavior

Throws an exception:

```
Environment:


Request Method: GET
Request URL: http://127.0.0.1:8001/

Django Version: 1.10.3
Python Version: 2.7.12
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'rest_framework',
 'schema_bug_test_app']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']



Traceback:

File "/Users/mlubimow/Projects/test_project/env/lib/python2.7/site-packages/django/core/handlers/exception.py" in inner
  39.             response = get_response(request)

File "/Users/mlubimow/Projects/test_project/env/lib/python2.7/site-packages/django/core/handlers/base.py" in _get_response
  187.                 response = self.process_exception_by_middleware(e, request)

File "/Users/mlubimow/Projects/test_project/env/lib/python2.7/site-packages/django/core/handlers/base.py" in _get_response
  185.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/Users/mlubimow/Projects/test_project/env/lib/python2.7/site-packages/django/views/decorators/csrf.py" in wrapped_view
  58.         return view_func(*args, **kwargs)

File "/Users/mlubimow/Projects/test_project/env/lib/python2.7/site-packages/django/views/generic/base.py" in view
  68.             return self.dispatch(request, *args, **kwargs)

File "/Users/mlubimow/Projects/test_project/env/lib/python2.7/site-packages/rest_framework/views.py" in dispatch
  477.             response = self.handle_exception(exc)

File "/Users/mlubimow/Projects/test_project/env/lib/python2.7/site-packages/rest_framework/views.py" in handle_exception
  437.             self.raise_uncaught_exception(exc)

File "/Users/mlubimow/Projects/test_project/env/lib/python2.7/site-packages/rest_framework/views.py" in dispatch
  474.             response = handler(request, *args, **kwargs)

File "/Users/mlubimow/Projects/test_project/env/lib/python2.7/site-packages/rest_framework/schemas.py" in get
  593.             schema = generator.get_schema(request)

File "/Users/mlubimow/Projects/test_project/env/lib/python2.7/site-packages/rest_framework/schemas.py" in get_schema
  242.         links = self.get_links(request)

File "/Users/mlubimow/Projects/test_project/env/lib/python2.7/site-packages/rest_framework/schemas.py" in get_links
  276.             insert_into(links, keys, link)

File "/Users/mlubimow/Projects/test_project/env/lib/python2.7/site-packages/rest_framework/schemas.py" in insert_into
  79.     target[keys[-1]] = value

Exception Type: TypeError at /
Exception Value: 'Link' object does not support item assignment
```



Discussion:
Any chance you could include the traceback of that exception?
Oh, yeah, sorry for that. I've updated report.
hello @tomchristie any idea when this will be solved?
Thanks
Not yet. Been focusing on functionality for 3.6 lately.
This also happens if an url  is (or maybe **also** ends with? did not try this one)  'list' (a natural verb enough).  The reason is that `insert_into` (in schemas.py) is passed a keys parameter whose last element's value  is 'list', which eventually conflicts with the 'list' present in the url. 
It also occurs when you use drf-extensions with NestedRoutes.
From what I remember it used to worked in 3.4.
Is it possible to have this resolved in the next patch release?
This issue is caused by usage of `url_path` in `detail_route` decorator. Any chance of fix?

```
class MyViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
    ...
    @detail_route(
        methods=['get', ],
        url_name='children-list',
        url_path='children(?:/(?P<child_id>[a-zA-Z0-9]+))?',
        serializer_class=emg_serializers.ChildSerializer
    )
    def children(self, , request, parent_id=None, child_id=None):
        ...
```
You can temporary fix it in your project using a custom `SchemaView` with a custom `SchemaGenerator`: https://stackoverflow.com/a/45323639/5157209
@carltongibson is it possible to get this fixed in 3.7.0?
<img width="695" alt="screenshot 2017-09-28 17 44 03" src="https://user-images.githubusercontent.com/64686/30976251-a4ab0c60-a474-11e7-9f7e-dbe3379ae716.png">

😀

that's the idea. 

I need to look into exactly what the issue is, and then the fix, but it's on [the short list](https://github.com/encode/django-rest-framework/milestone/49)

Resolving patch:
diff --git a/rest_framework/schemas/generators.py b/rest_framework/schemas/generators.py
--- a/rest_framework/schemas/generators.py
+++ b/rest_framework/schemas/generators.py
@@ -51,6 +51,19 @@ def is_api_view(callback):
     return (cls is not None) and issubclass(cls, APIView)
 
 
+INSERT_INTO_COLLISION_FMT = """
+Schema Naming Collision.
+
+coreapi.Link for URL path {value_url} cannot be inserted into schema.
+Position conflicts with coreapi.Link for URL path {target_url}.
+
+Attemped to insert link with keys: {keys}.
+
+Adjust URLs to avoid naming collision or override `SchemaGenerator.get_keys()`
+to customise schema structure.
+"""
+
+
 def insert_into(target, keys, value):
     """
     Nested dictionary insertion.
@@ -64,7 +77,15 @@ def insert_into(target, keys, value):
         if key not in target:
             target[key] = {}
         target = target[key]
-    target[keys[-1]] = value
+    try:
+        target[keys[-1]] = value
+    except TypeError:
+        msg = INSERT_INTO_COLLISION_FMT.format(
+            value_url=value.url,
+            target_url=target.url,
+            keys=keys
+        )
+        raise ValueError(msg)
 
 
 def is_custom_action(action):


Test patch:
diff --git a/tests/test_schemas.py b/tests/test_schemas.py
--- a/tests/test_schemas.py
+++ b/tests/test_schemas.py
@@ -6,13 +6,15 @@
 from django.http import Http404
 from django.test import TestCase, override_settings
 
-from rest_framework import filters, pagination, permissions, serializers
+from rest_framework import (
+    filters, generics, pagination, permissions, serializers
+)
 from rest_framework.compat import coreapi, coreschema
 from rest_framework.decorators import (
     api_view, detail_route, list_route, schema
 )
 from rest_framework.request import Request
-from rest_framework.routers import DefaultRouter
+from rest_framework.routers import DefaultRouter, SimpleRouter
 from rest_framework.schemas import (
     AutoSchema, ManualSchema, SchemaGenerator, get_schema_view
 )
@@ -20,7 +22,9 @@
 from rest_framework.test import APIClient, APIRequestFactory
 from rest_framework.utils import formatting
 from rest_framework.views import APIView
-from rest_framework.viewsets import ModelViewSet
+from rest_framework.viewsets import GenericViewSet, ModelViewSet
+
+from .models import BasicModel
 
 factory = APIRequestFactory()
 
@@ -726,3 +730,81 @@ def get(self, request, *args, **kwargs):
             "The `OldFashionedExcludedView.exclude_from_schema` attribute is "
             "pending deprecation. Set `schema = None` instead."
         )
+
+
+@api_view(["GET"])
+def simple_fbv(request):
+    pass
+
+
+class BasicModelSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = BasicModel
+        fields = "__all__"
+
+
+class NamingCollisionView(generics.RetrieveUpdateDestroyAPIView):
+    queryset = BasicModel.objects.all()
+    serializer_class = BasicModelSerializer
+
+
+class NamingCollisionViewSet(GenericViewSet):
+    """
+    Example via: https://stackoverflow.com/questions/43778668/django-rest-framwork-occured-typeerror-link-object-does-not-support-item-ass/
+    """
+    permision_class = ()
+
+    @list_route()
+    def detail(self, request):
+        return {}
+
+    @list_route(url_path='detail/export')
+    def detail_export(self, request):
+        return {}
+
+
+naming_collisions_router = SimpleRouter()
+naming_collisions_router.register(r'collision', NamingCollisionViewSet, base_name="collision")
+
+
+class TestURLNamingCollisions(TestCase):
+    """
+    Ref: https://github.com/encode/django-rest-framework/issues/4704
+    """
+    def test_manually_routing_nested_routes(self):
+        patterns = [
+            url(r'^test', simple_fbv),
+            url(r'^test/list/', simple_fbv),
+        ]
+
+        generator = SchemaGenerator(title='Naming Colisions', patterns=patterns)
+
+        with pytest.raises(ValueError):
+            generator.get_schema()
+
+    def test_manually_routing_generic_view(self):
+        patterns = [
+            url(r'^test', NamingCollisionView.as_view()),
+            url(r'^test/retrieve/', NamingCollisionView.as_view()),
+            url(r'^test/update/', NamingCollisionView.as_view()),
+
+            # Fails with method names:
+            url(r'^test/get/', NamingCollisionView.as_view()),
+            url(r'^test/put/', NamingCollisionView.as_view()),
+            url(r'^test/delete/', NamingCollisionView.as_view()),
+        ]
+
+        generator = SchemaGenerator(title='Naming Colisions', patterns=patterns)
+
+        with pytest.raises(ValueError):
+            generator.get_schema()
+
+    def test_from_router(self):
+        patterns = [
+            url(r'from-router', include(naming_collisions_router.urls)),
+        ]
+
+        generator = SchemaGenerator(title='Naming Colisions', patterns=patterns)
+
+        with pytest.raises(ValueError):
+            generator.get_schema()