using url.reverse() has side effects

Disclaimer: this is a minimal reproducible example, my actual code is a lot more complicated.

Consider the following test:

from django.urls import reverse

def test_foo():
    url = reverse("my_app:foo")
    assert url == "/my_app/foo/"

Everything is configured properly and the test passes.

Now I have different test, with the following code:

from django.contrib import admin
from django.urls import reverse
from tests.test_app.models import TestModel
def test_bar():
    admin.site.register(TestModel, admin.ModelAdmin)
    url = reverse("admin:test_app_testmodel_changelist")
    assert url == "/admin/test_app/testmodel/"

This test, when run alone or before test_foo(), passes.

However, if test_bar() is run after test_foo(), then it fails with an exception django.urls.exceptions.NoReverseMatch: Reverse for 'test_app_testmodel_changelist' not found. 'test_app_testmodel_changelist' is not a valid view function or pattern name.

This example can be simplified:

def test_baz_passes():
    admin.site.register(TestModel, admin.ModelAdmin)
    url = reverse("myapp:foo")
    url = reverse("admin:test_app_testmodel_changelist")

def test_baz_fails():
    url = reverse("myapp:foo")
    admin.site.register(TestModel, admin.ModelAdmin)
    url = reverse("admin:test_app_testmodel_changelist")

Clearly there are some side effects (caches?) done in urls.reverse(), which are not reversed/cleared by registering admin app. The result is that subsequent calls to reverse() don’t find new urls provided by ModelAdmin. I did try to run clear_url_caches() after registering site, but it does not help.
I’ve found one ticket comment that vaguely resembles this case, but I’m not sure if that is helpful.

Is this a bug?

Hi @piotr-kubiak.

Register is meant to be called at import time, before the URL conf is loaded. I’d have to go hunting to see how to properly clear the loaded URLs in the way you’re trying to do.

I think your best bet is to create a separate admin site for the test you want to use, and then set the URL conf using override_settings there. (I.e. you’d add the custom site’s URLs to urlpatterns in that case.).

Thanks. I’ve managed to clear the URLs with:

from importlib import reload
reload(sys.modules["myapp.urls"])

However I find this a little bit hacky. I will explore the second AdminSite solution.

But I still think there is a bug. If admin.site.register() should not be called after import time, then in my opinion:

  • this limitation should at least be mentioned in the docs, or
  • register() should by itself check and report if it is called after import time, since it is useles otherwise, or
  • register() should by itself make the effort to clear URL cache, so that every subsequent call to resolve() is correct.

You’d be welcome to look into it. I’d suggest looking at how Django’s own test suite does it. (Hence my suggestion of the multiple admin sites…)

I’m not immediately convinced that the docs, which show correct usage, would benefit from mentioning every possible misuse. But🤷‍♀️