Opened 15 months ago

Last modified 4 months ago

#21927 new Cleanup/optimization

URL namespacing improvements

Reported by: aaugustin Owned by: nobody
Component: Core (URLs) Version: master
Severity: Normal Keywords:
Cc: real.human@…, bendavis78, dries@…, apollo13, info+coding@… Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

I discussed URL namespacing with Malcolm at DjangoCon US 2012. I took some notes on the current design and possible improvements.

Unfortunately, I haven't found the time and motivation to actually work on these improvements since then. I just mentioned them in #20734.

I'm including below a slightly edited version of my notes. Not everything is clear, but that's all I have, and I don't remember anything else.


Use cases

URL namespaces only exist for the purpose of reversing. They're "names for groups of URLs".
(They're analogous to XML namespaces in this regard.)

  • Apps need to be able to reverse their own URLs, even if there are several instances installed.
  • It must be possible to find the default instance of an app.
  • It must be possible to find a specific instance of an app.

Application vs. instance namespaces

An application namespace = app_name

  • There can be only one in a given project.
  • The only use case for not using the application label is name conflicts.
  • Shouldn't it be eventually moved to app._meta? (not sure about what I meant there)

An instance namespace = namespace

  • It differentiate instances of the same application.

Next steps

1) Clarify documentation

2) Make it possible to reverse without specifying the namespace and document this pattern:

urlpatterns = (
    url(r'^foo/', include('foo.urls', namespace='foo')),
)

This requires a way to specifiy the default namespace. It would supersede #11642.

Change History (13)

comment:1 Changed 13 months ago by alanwj

I use the following in most of my projects, which makes namespaces work more or less the way people probably expect them to. It allows you to define an app_name (and optionally a namespace) in your urlconf, and falls back to the current behavior if you don't.

from django.conf.urls import include as django_include
from django.utils.importlib import import_module
from django.utils import six


def include(urlconf_module, namespace=None, app_name=None):
    """Namespace aware replacement for Django's include."""
    if isinstance(urlconf_module, six.string_types):
        urlconf_module = import_module(urlconf_module)
    app_name = app_name or getattr(urlconf_module, 'app_name', None)
    namespace = namespace or getattr(urlconf_module, 'namespace', None)

    # app_name doesn't work unless namespace is also defined
    return django_include(urlconf_module, namespace or app_name, app_name)

comment:2 Changed 12 months ago by bendavis78

A major problem i see with url namespaces is that if the user of an app specifies a namespace, it will break url reversals within the app itself (even when using current_app). If I want my app to support namespacing, and I don't want my url reversals to break within my app, I have to provide a function that accepts a namespace argument and returns a triple and instruct the user to use that instead of specifying a namespace in in include(). This makes the namespace argument to include() fairly useless, IMHO. It also forces a non-standard API for namespacing urls.

If what this ticket proposes can fix that, I'm all for it.

comment:3 Changed 12 months ago by mrmachine

Indeed, an app author ideally shouldn't need to do anything special (hard coding the app name or namespace) when reversing the app's own URLs because it is at the project level, outside the control of the app author, where app URLs are installed into the root URLconf and optionally with a namespace, either to avoid conflict with another installed app or to install one app more than once at different URLs.

This means that app authors must provide instructions to the effect that "my app must be installed within a namespace with an app_name and at least once with a namespace of FOO" or some other work-around like the one you have mentioned.

Django should automatically provide a default "current app" hint to reverse() and {% url %} when URLs are reversed within the context of a request/response cycle where the requested URL is installed into the root URLconf with a namespace. Django can get this information from request.resolver_matchearly in the request/response cycle and make it available to reverse() via thread local storage. The use of thread local storage is just one idea, I would be open to any others.

Apps should not themselves need to know anything about the namespace that they are installed into (if any). App authors should reverse URLs using their name only, as defined in the app's URLconf, and project developers should be able to install that URLconf with any arbitrary namespace as they see fit. Django should know when a URL is being reversed within the context of that app, and provide the current app hint automatically based on the configuration provided by the project developer.

See #22203 which proposes a default current app via thread local storage but was closed as wontfix, and the corresponding google groups thread: https://groups.google.com/forum/#!topic/django-developers/mPtWJHz2870

Also see #11642 which proposes to allow app authors to define a default app name/namespace, similar to the earlier comment above by alanwj.

comment:4 Changed 12 months ago by mrmachine

  • Cc real.human@… added

comment:5 Changed 11 months ago 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:

# 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():

#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):

# 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:

In [1]: from django.core.urlresolvers import reverse
In [2]: reverse('somethings:usefullapp_one_thing')
Out[2]: '/some-things/one-thing/'
In [3]: from usefulapp import get_thing_url
In [4]: get_thing_url(current_app='somethings')
Out[2]: '/some-things/one-thing/'

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:

#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)),
]
In [1]: from django.core.urlresolvers import reverse
In [2]: reverse('usefullapp_one_thing')
...
NoReverseMatch: Reverse for 'usefullapp_one_thing' with arguments '()' and keyword arguments '{}' not found.

“Hmm, that's odd. Oh, right this app uses namespaces, like with the admin and "admin:index"... Seems like this should work...”

In [2]: reverse('usefulapp:usefullapp_one_thing')
...
NoReverseMatch: u'passreset' is not a registered namespace
“What the... How the heck do I register a namespace? The docs 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:

# 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()

# 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:

<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):

# 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:

#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).

Version 0, edited 11 months ago by bendavis78 (next)

comment:6 Changed 11 months ago by mrmachine

One weird thing about the examples above (which I completely agree with), why is the app name AND a current_app hint required for reverse but not for {% url %}? E.g. reverse('usefulapp:another_thing', current_app=current_app) vs {% url "another_thing" %} (with the current_app hint set on the context)?

I really think that the current state of namespacing is broken. Yes, it was intended to allow multiple deployments of the same app, but there is an obvious and legitimate desire for app authors to name their URLs uniquely *within their app*, and allow project authors to specify a namespace to avoid conflicts between apps that know nothing about each other.

Currently, this requires explicit additional repetitive work by app authors during development *and* explicit installation instructions to project authors.

Django knows which URL matches the request path. It knows the namespace that a project author might have optionally specified for that URLconf. Django should use that automatically when using reverse() and {% url %}. App authors can then write their apps without any worry about additional code or explicit installation instructions. They can name their URLs without worrying about conflicts with other apps, as long as they are unique within *their* app. Then project authors can install any app with any namespace they like.

comment:7 Changed 11 months ago by bendavis78

@mrmachine, current_app isn't necessarily available in every template context. It has to be explicitly set. For example, contrib.admin sets current app as a property on AdminSite, and explicitly adds it to the context. contrib.auth passes around current_app in its view kwargs, and explicitly sets it when rendering templates.

I don't think "keeping an apps url's from conflicting with each other" is a reason to fix namespaces, as you can already do that with urlname prefixes. The reason namespaces need fixing is to improve the API for deploying multiple instances of the same apps at different urls. It just so happens it has the added side effect/benefit of allowing devs to avoid prefixing urlnames.

IMO the API needs to be changed so that all urls are put into a namespace, the default of which is the application's app_label. That would maket things much easier for app users and app devs alike.

comment:8 Changed 11 months ago by bendavis78

  • Cc bendavis78 added

comment:9 Changed 11 months ago by mrmachine

@bendavis78, I agree that "keeping an app's URLs from conflicting with each other" alone is not reason enough for significant change. But it is a nice side effect, and not one that should deter us from making significant change.

On current_app, Django could set a default hint for it in request middleware (or before middleware runs) in thread local storage. Then Django could use that hint (again, as a default only) any time reverse() or {% url %} is called.

There is a problem with forcing ALL included app URLs into a namespace. It makes it impossible for project developers to override the location of a particular URL provided by a particular app. For example, generic app foo is installed at /foo/. It provides a URL named bar at /foo/bar/. If a project developer wants to shorten just that URL to /bar/, he can't if it has been installed in a namespace.

If this problem could be worked around, e.g. by allowing a namespace to be given to a single URL included in the root URLconf (not only when one URLconf is included into another), and have that URL be detected first and therefore overriding the version from the included URLconf, then I would wholeheartedly support your proposal of giving ALL included URLs a default namespace that matches their app label.

comment:10 Changed 8 months ago by driesdesmet

  • Cc dries@… added

comment:11 Changed 4 months ago by aaugustin

According to the summary, "the only use case for not using the application label [as the application namespace] is name conflicts".

If that's correct, we can drop application namespaces because unicity of application labels is enforced since Django 1.7.

comment:12 Changed 4 months ago by apollo13

  • Cc apollo13 added

comment:13 Changed 4 months ago by MarkusH

  • Cc info+coding@… added
Note: See TracTickets for help on using tickets.
Back to Top