#21927: URL namespacing improvements
--------------------------------------+------------------------------------
     Reporter:  aaugustin             |                    Owner:  nobody
         Type:  Cleanup/optimization  |                   Status:  new
    Component:  Core (URLs)           |                  Version:  master
     Severity:  Normal                |               Resolution:
     Keywords:                        |             Triage Stage:  Accepted
    Has patch:  0                     |      Needs documentation:  0
  Needs tests:  0                     |  Patch needs improvement:  0
Easy pickings:  0                     |                    UI/UX:  0
--------------------------------------+------------------------------------

Comment (by bendavis78):

 I always get confused when revisiting URL namespaces, and I feel like I
 have to re-learn how they work each time. That tells me there's something
 wrong with the API here. I'm commenting here mostly just so that I can
 come back next time I get confused. Maybe this will help explain the issue
 to others.

 Whether or not you're using namespaces, ''current'' best practice dictates
 that a url name should always prefixed in some way to keep it from
 clashing with other names in the app:

 {{{
 #!python
 # usefulapp/urls.py
 urlpatterns = [
     url('^one-thing/$', views.one_thing, name='usefulapp_one_thing'),
     url('^another-thing/$', views.another_thing,
 name='usefulapp_another_thing')
 ]
 }}}

 This in itself is a manner of namespacing, but has nothing to do with
 Django's url namespaces. The purpose of url namespaces is not to keep one
 app's urls clashing with another's, but to allow an app's urlconf module
 to be used multiple times in a project via `include()`:

 {{{
 #!python
 #some_big_project/root_urlconf.py
 import usefulapp.urls

 urlpatterns = [
     url('^$', my_project_app.views.home),
     url('^random_page', my_project_app.views.random_page),
     url('^some-things', include(usefulapp.urls, namespace='somethings')),
     url('^other-things', include(usefulapp.urls, namespace='otherthings'))
 ]
 }}}

 As an **app developer**, I don't explicitly define any namespace. If I
 want my app to support multiple inclusions of its urlconf, I must reverse
 my urls using the "application namespace" (which is always my app_label):

 {{{
 #!python
 # usefulapp/utils.py
 from django.core.urlresolvers import reverse

 def get_thing_url(current_app):
    #                                       The ‘instance namespace’ ╮
    #                                                           ╭────┴────╮
    return reverse("usefulapp:usefulapp_one_thing",
 current_app=current_app)
    #              ╰────┬────╯
    #                   ╰ the ‘application namespace’ (defaults to
 "usefulapp")

 }}}

 **Problem # 1:** If I reverse urls in my app without using the application
 namespace, my app will be broken for those who wish to use namespacing.

 A **project developer** can then reverse urls as needed using the
 namespace:

 {{{
 #!html
 <div class="code"><pre style="background: white; color:black; padding:
 10px">
 <span style="color:green">In [1]:</span> from django.core.urlresolvers
 import reverse
 <span style="color:green">In [2]:</span>
 reverse('somethings:usefullapp_one_thing')
 <span style="color:red">Out[2]:</span> '/some-things/one-thing/'
 <span style="color:green">In [3]:</span> from usefulapp import
 get_thing_url
 <span style="color:green">In [4]:</span>
 get_thing_url(current_app='somethings')
 <span style="color:red">Out[2]:</span> '/some-things/one-thing/'
 </pre></div>
 }}}

 Ok, that's all fine and dandy. **But**, what about when another project
 developer comes along and wants to use my app, and doesn't really care
 about namespacing?

 **Problem # 2:** This is how the vast majority of Django include an app in
 their urls:
 {{{
 #!python
 #my_project/root_urlconf.py
 import usefulapp.urls

 urlpatterns = [
     url('^$', my_project_app.views.home),
     url('^random_page', my_project_app.views.random_page),
     url('^some-things', include(usefulapp.urls)),
 ]
 }}}
 {{{
 #!html
 <div class="code"><pre style="background: white; color:black; padding:
 10px">
 <span style="color:green">In [1]:</span> from django.core.urlresolvers
 import reverse
 <span style="color:green">In [2]:</span> reverse('usefullapp_one_thing')
 ...
 <span style="color:red; font-weight: bold">NoReverseMatch</span>: Reverse
 for 'usefullapp_one_thing' with arguments '()' and keyword arguments '{}'
 not found.
 </pre>
 }}}
 “Hmm, that's odd. Oh, right this app uses namespaces, like with the admin
 and "admin:index"... Seems like this should work...”
 {{{
 #!html
 <div class="code"><pre style="background: white; color:black; padding:
 10px">
 <span style="color:green">In [2]:</span>
 reverse('usefulapp:usefullapp_one_thing')
 ...
 <span style="color:red; font-weight: bold">NoReverseMatch</span>:
 u'passreset' is not a registered namespace
 </pre>
 }}}
 {{{
 #!html
 “What the... How the heck do I register a namespace? The <a
 href="https://docs.djangoproject.com/en/dev/topics/http/urls/";
 target="_blank">docs</a> never said anything about registering
 namespaces.”
 }}}

 (they really don't)

 TLDR; The problem is, by supporting namespacing in the app, I'm forcing
 all other users to explicitly declare an instance namepsace even if they
 don't need one. The implementation of the API may be simple, but the usage
 is complicated and confusing. Ideally, an app developer shouldn't have to
 worry about whether or not someone uses a namespace with their app. A
 project developer shouldn't have to use the namespace arg if it isn't
 necessary.

 It's been said in previous tickets that a "default namespace" will
 encourage developers to create "poor url patters" like `url(...,
 name=post)`. As for me, I don't see this as a poor url pattern; it's
 simple, clean, and easy to read. It's no surprise a developer would want
 to do this. The admin does it, why can't anyone else?

 It all comes down to how the app is deployed. As an app developer I can
 solve the above problems by using the following patterns, which are
 simplified variants on what `contrib.admin` is doing:

 {{{
 #!python
 # usefulapp/__init__.py

 app_label = __name__.split('.')[-1]
 default_ns = app_label  # our default *instance* namespace

 def urls_ns(namespace=default_ns):
     """
     Returns a 3-tuple of url patterns registered under the given namespace
 for use with include().
     """
     # In for to use reverse() in our views, we need to provide them
 `current_app`
     kwargs = {'current_app': default_ns}
     urlpatterns = [
         url('^one-thing/$', views.one_thing, name='one_thing',
 kwargs=kwargs),
         url('^another-thing/$', views.another_thing, name='another_thing',
 kwargs=kwargs)
     ]
     return (urpatterns, app_label, namespace)

 # Allow the use of `include(usefulapp.urls)`.
 # Note that we don't have a urls.py in the top-level package.
 urls = urls_ns()
 }}}

 Our views must accept the current_app kwarg so that we can use reverse()
 {{{
 #!python
 # usefulapp/views.py

 def one_thing(request, current_app=None):
    context = {
        'current_app': current_app
        'another_url': reverse('usefulapp:another_thing',
 current_app=current_app)
    }
    return render(request, 'usefulapp/one_thing.html', context)


 def another_thing(request, current_app=None):
    context = {'current_app': current_app}
    return render(request, 'usefulapp/another_thing.html', context)
 }}}

 Since our context now has `current_app` in it, the {% url %} tag will work
 without it:
 {{{
 #!django
 <a href="{% url "another_thing" %}">Well isn't that special...</a>
 }}}

 Now, an app developer can deploy our app without having to worry about
 registering a namespace (the registration occurs by including the
 3-tuple):
 {{{
 #!python
 # my_project/root_urlconf.py
 import usefulapp

 urlpatterns = [
     #...
     url('^some-things', include(usefulapp.urls))
 ]
 }}}

 *But* if they want to use namespacing, they can't use the `namespace`
 kwarg with our `urls` tuple. The `include()` function forbids re-
 namespacing a 3-tuple, though I'm not sure why. The solution is to
 instruct them to use of our `urls_ns()` function instead:

 {{{
 #!python
 #some_big_project/urls.py

 urlpatterns = [
     #...
     url('^some-things', include(usefulapp.urls_ns('somethings')),
     url('^other-things', include(usefulapp.urls_ns('otherthings'))
 ]
 }}}

 I think a the solution for this would involve a more robust API for
 defining urlpatterns, possibly involving AppConfig. It would be nice if we
 didn't have to do so much passing around of `current_app`, but I'm not
 sure how we'd make that more transparent.

 Also, I think the `app_name` argument is pretty pointless -- I can't
 imagine a use case of changing `app_name` that wouldn't be covered by just
 creating another namespace. Unless there's a legitimate use case for
 changing it, it should not be part of the public API (let me know if I'm
 wrong here).

-- 
Ticket URL: <https://code.djangoproject.com/ticket/21927#comment:5>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/067.49af43f2f8dbe8f4f38efdfc91995534%40djangoproject.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to