#21927 closed Cleanup/optimization (fixed)
URL namespacing improvements
Reported by: | Aymeric Augustin | Owned by: | Marten Kenbeek |
---|---|---|---|
Component: | Core (URLs) | Version: | dev |
Severity: | Normal | Keywords: | |
Cc: | real.human@…, Ben Davis, dries@…, Florian Apolloner, info+coding@…, marten.knbk@… | Triage Stage: | Accepted |
Has patch: | yes | 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 (24)
comment:1 by , 11 years ago
comment:2 by , 11 years ago
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 by , 10 years ago
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_match
early 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 by , 10 years ago
Cc: | added |
---|
comment:5 by , 10 years ago
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).
comment:6 by , 10 years ago
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 by , 10 years ago
@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 by , 10 years ago
Cc: | added |
---|
comment:9 by , 10 years ago
@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 by , 10 years ago
Cc: | added |
---|
comment:11 by , 10 years ago
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 by , 10 years ago
Cc: | added |
---|
comment:13 by , 10 years ago
Cc: | added |
---|
comment:14 by , 9 years ago
Cc: | added |
---|---|
Owner: | changed from | to
Status: | new → assigned |
comment:16 by , 9 years ago
Patch needs improvement: | set |
---|
comment:17 by , 9 years ago
Patch needs improvement: | unset |
---|
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.