Bringing Locality of Behaviour to Django Views and URLs

Hi y’all

I’ve been working on a package called django-view-decorator which bridges the gap in locality of behaviour between views and URLs. To introduce this package I have written a blog post, which I in turn want to use as a starting point for a discussion about getting the pattern implemented by said package into Django core.

At it’s core django-view-decorator enables you to write

@view(paths="foo/", name="foo")
def foo(request: HttpRequest) -> HttpResponse:
    return HttpResponse("foo")

instead of placing views and their corresponding URLs in different files.

The blog post is here:

https://valberg.dk/bringing-locality-of-behaviour-to-django-views-and-urls.html

I want to use this Django forum topic as a place to discuss the pattern and the idea about doing something similiar in Django itself.

So, if you have the time and have any feedback, ideas, criticism, please do share them here :slight_smile:

Cheers
Víðir

6 Likes

I have been working in something similar for my own use, I thought nobody care about that, but for me is the most unproductive part of Django MVT pattern, going time after time to the urls.py file.

IMO is a great idea. In order to keep thing simply I encourage you to focus only in routes.

1 Like

Hi @valberg. Nice post.

I’ve moved this to “Django Internals” since it’s about what to include in Django.

I have to admit I’m not keen on the decorator approach to URL routing. A couple of reasons…

  1. It makes it impossible to get an overview of your URL patterns. For small apps it’s not a problem, but in bigger apps you simply can’t see what URLs are defined. (To this extent it has worse locality of behaviour… :slightly_smiling_face:) I much prefer have a single list of urlpatterns that acts as the table of contents for the application. (Massive aside: I’ll note here that Starlette recently deprecated the use of the route decorator, in preference for a single list of routes.)
  2. It makes view resolution dependent on source code and then import order. If I need to force a specific view earlier than a more general one, I’m moving the view declaration, rather than tweaking my list. Doing that I need to hold the resultant list in my head, rather than being able to read it (which is point 1 again.)

I do agree that there’s improvements to be made here. The URL name, used in reverse, and likely get_absolute_url(), and the url template tag is a real pain, binding everything together at every level, for example. I’d like to see progress made there (if we can). I’m just not sure a view decorator is the way to do that.

(In Neapolitan, the URL name issue is mitigated by a convention of known model name and action patterns, but that doesn’t generalise beyond the CRUD cases.)

I liked your point about the AppConfig holding e.g. namespace details — I shall ponder that.

Kind Regards,

Carlton

1 Like

Hi @carltongibson! Thanks for the feedback! :blush:

Regarding your two points:

  1. I have thought the same thing - so solve this django-view-decorator has a management command which lists all registered URLs with a path to their associated view. In my opinion this is a much better way to find what view a URL is pointing at than searching through a massive nested list of path and include calls. I would rather have to run a command to get an overview over URLs once in a while and get LoB for when I’m working with the individual view.

  2. Great point! I haven’t thought about that. I can see that the example in the documentation (URL dispatcher | Django documentation | Django) illustrates this quite well. I haven’t made use of such URLs myself, and would probably put the special logic into the view itself (ie. if year == 2003:). All solutions I can think of which can mitigate this “problem” are in the “too complicated” category. So for now I would say it is a price to pay :slight_smile:

I guess, as in many API design situations, it comes down to preference. Do we want A at the cost of B. I wonder if there is a middle-ground where we can have the best of both worlds. Maybe the urlpatterns could be an opt-in to get more control over URLs, with the decorator approach as the “standard”, or vice versa. I don’t know if that would work since it would mean two ways of doing URLs.

See Should the show_urls command be part of Django's standard library? — there’s an open accepted ticket, with what looks like a good amount of community support, to add such a management command already. (If you wanted to add that quickly, it could likely make it for Django 5.0 :racing_car:)

The issue (here) with such a command (which we should add independently) is it’s not editable, so you’re still digging down to the relevant view file, and moving views (or adjusting imports) to change the ordering.

I agree that once you’ve got includes and what not it gets complicated. But it gets complicated anyway. My take, my experience, is that the unified patterns list helps to manage this, whereas the decorator pattern makes it worse. (I accept I can’t force you to agree to that last point. :blush:)

1 Like

I like having a “Table of Contents”-type urls listing too, but I have also thought in the past that it would be nice to try the Flask-like method of url declaration. I’m thinking in terms of code navigation and tooling in what I’m writing here: When in the urls.py file, it is easy to click on the view name (at least in VS Code) to go straight to the view declaration, but when in the views.py file, there is not an equivalent quick-navigation method (that I’m aware of) to get to the corresponding url in urls.py (and I do find that somewhat annoying). While tooling is no substitute for core functionality, if I were to use django-view-decorator, I would gladly use a VS Code plugin that supplements the VS Code Explorer view to show a list of urls found in a views.py file. I would prefer this over a management command that lists urls because (ideally) a url shown in an Explorer list would be clickable to quickly go to that point in the file. Also, from a core functionality standpoint, what would help me the most in adopting django-view-decorator would be a management command that could be run against an existing project to automatically annotate all views.py files to the greatest extent possible. I would much rather have this management command to do auto-annotation than a management command that lists urls. A VS Code plugin to create a clickable list of urls would be ideal in terms of tooling, but the Collapse All command in VS Code might also serve to sort of show a list of urls - it just wouldn’t be a urls-only list because the view declarations would be interspersed.

I think it would be great if both methods of URL declaration could be a part of Django at some point in order to give the choice to developers. Also, perhaps fully implementing this feature in Django core would make it more likely that Flask developers would try Django and ultimately end up using it instead of Flask. That would be good for Django’s future. While Django has been breaking away from Flask in the last two years in terms of Github stars, I would like to see Django’s growth accelerate further, and adding such Flask-like core functionality could improve that possibility. (Note that this type of url declaration is also how the next-closest competitor in the Github stars list, FastAPI, declares urls, so to me, that provides one more reason to implement this pattern.)

1 Like

I’m strongly against this idea.

I came to Django from working in two different environments that tied the definition of the url to the function handling that url - and I found it overall to be a distinct disadvantage. The difficulties that it created for us far outweighed any theoretical benefits that might be claimed by the idea of “locality of behavior”.

In addition to the points that Carlton pointed out, I’ll add that you can have multiple urls referring to the same view - with different default parameters, that any url be associated with a particular app (urls do not need to be structured to match your apps), and that urls can be dynamically generated in a manner similar to how the admin works.

If someone wanted to make that a 3rd party module, fine. But I hope it never makes it into core - I think it would be a huge step back.

2 Likes

When I was working on this, my approach was to compile from views to the urls file with a custom django-admin command . Something similar to migrations but with urls. At the end I chose to comment out all my views with the pattern and the name of the function declared in urls because I didn’t want to mantain that only for me.
In fact a custom command like this could help in order to take track of duplicated url patterns, could connect url patterns of declared apps in settings file automatically and keep some opinionated patters with routes.
For example the command could build a app_name and a url “include” based on the name of the app, a pattern and url name based on the type of the view (Specially for CBV).
I mean, there are a lot of other thinks like this in Django, the “strartapp” command make a opinionated scaffolding for and app, the edit CBV’s has lot of default behavior (template_name, fields, etc.), but in the urls there’s nothing that helps and make you have good patterns from start and of course that can be change from its default behavior.
If the command code the route from the function or class name with some rules, it could be easier to know what is your url name and pattern. Something similar with what happens when you use Django Admin (you can know the urls) or when you look for a template name of a CBV, you know what to look for.

I’m also of the opinion that this shouldn’t be added to core. I think it’s okay in limited cases for “sub frameworks” - like how DRF, the admin, or Neapolitan generate URLs from view functions that may be decorated.

I agree it would be nice to have better tooling around jumping between views and URLs, but that seems like a code editor problem to me. For example, PyCharm links views and templates, it could do the same with URLs.

4 Likes

I think this is DRY at its best. I’ve written (and am maintaining) quite a few Django apps for various purposes and planned to write something very similar myself. I think, though, your solution is better and more mature anyway (and saves me the hassle :wink: ). I’ll be happy to try it out and wouldn’t expect major problems.

Of course it shouldn’t replace the current approach; those who are happy with the latter needn’t bother. Every now and then I stumbled over disagreements between urls.py and views.py because I changed the one and not the other (and the runserver crashes if urls.py changes first).

Right, the sequence of path entries in urls.py can matter, but I always try to avoid situations where the sequence is significant – I expect that to get me into troubles sooner or later. Of course people’s mileage may vary; I’ve always advised beginners to make sure their paths are disjunct.

I’d actually like to see this upstreamed, but as a third-party module it’s also fine.

Since your decorator can find out about the namespace, it’d be nice to also have a namespace-aware reverse() utility (and get_absolute_url()). They could probably get at the namespace in the same way (through a factory function) as the view decorator. I don’t see, however, how the namespace could be encapsulated in (a variant of) the url template tag.

1 Like

I’ve never been a fan of how fastapi and similar frameworks declare URL routes. I believe Django already has the best approach. I understand that having the URL declaration just above the view definition would enhance the Locality of Behavior, and it would be convenient not to have to jump to another file to see the URLs associated with a specific view. However, I don’t think Locality of Behavior is something that should be applied universally. Lately, I’ve come across some questionable ideas (not necessarily related to this one or within the Python community) in the name of LOB.

There is a simple trick that I use to navigate to the URLs related to a specific view function, and it should work in most editors (though I have only tried it in PyCharm). Just jump to the function usages—usually, view functions are only used to define URL paths—so if you try to jump to the function’s usages, it will take you back to the path linked to that function.

There is another project that adds a similiar decorator based approch to url configuration in Django- Django Ninja. https://django-ninja.rest-framework.com/

It is probably worth raking a look at their approach.