diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py index 178d2737d2..eba1536580 100644 --- a/django/urls/resolvers.py +++ b/django/urls/resolvers.py @@ -164,6 +164,7 @@ class RegexURLResolver(LocaleRegexProvider): # urlpatterns self._callback_strs = set() self._populated = False + self._populating = False def __repr__(self): if isinstance(self.urlconf_name, list) and len(self.urlconf_name): @@ -177,6 +178,9 @@ class RegexURLResolver(LocaleRegexProvider): ) def _populate(self): + if self._populating: + return + self._populating = True lookups = MultiValueDict() namespaces = {} apps = {} @@ -209,7 +213,9 @@ class RegexURLResolver(LocaleRegexProvider): namespaces[namespace] = (p_pattern + prefix, sub_pattern) for app_name, namespace_list in pattern.app_dict.items(): apps.setdefault(app_name, []).extend(namespace_list) - self._callback_strs.update(pattern._callback_strs) + if not pattern._populating: + pattern._populate() + self._callback_strs.update(pattern._callback_strs) else: bits = normalize(p_pattern) lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args)) @@ -219,6 +225,7 @@ class RegexURLResolver(LocaleRegexProvider): self._namespace_dict[language_code] = namespaces self._app_dict[language_code] = apps self._populated = True + self._populating = False @property def reverse_dict(self): diff --git a/tests/admin_docs/namespace_urls.py b/tests/admin_docs/namespace_urls.py new file mode 100644 index 0000000000..d05922c33e --- /dev/null +++ b/tests/admin_docs/namespace_urls.py @@ -0,0 +1,14 @@ +from django.conf.urls import include, url +from django.contrib import admin + +from . import views + +backend_urls = ([ + url(r'^something/$', views.XViewClass.as_view(), name='something'), +], 'backend') + +urlpatterns = [ + url(r'^admin/doc/', include('django.contrib.admindocs.urls')), + url(r'^admin/', admin.site.urls), + url(r'^api/backend/', include(backend_urls, namespace='backend')), +] diff --git a/tests/admin_docs/tests.py b/tests/admin_docs/tests.py index 3d5955d680..82384a4bb8 100644 --- a/tests/admin_docs/tests.py +++ b/tests/admin_docs/tests.py @@ -88,6 +88,12 @@ class AdminDocViewTests(TestDataMixin, AdminDocsTestCase): # View docstring self.assertContains(response, 'Base view for admindocs views.') + @override_settings(ROOT_URLCONF='admin_docs.namespace_urls') + def test_namespaced_view_detail(self): + url = reverse('django-admindocs-views-detail', args=['admin_docs.views.XViewClass']) + response = self.client.get(url) + self.assertContains(response, '

admin_docs.views.XViewClass

') + def test_view_detail_illegal_import(self): """ #23601 - Ensure the view exists in the URLconf. diff --git a/tests/urlpatterns_reverse/nested_urls.py b/tests/urlpatterns_reverse/nested_urls.py new file mode 100644 index 0000000000..f41b1449a8 --- /dev/null +++ b/tests/urlpatterns_reverse/nested_urls.py @@ -0,0 +1,25 @@ +from django.conf.urls import include, url +from django.views import View + + +def view1(request): + pass + + +def view2(request): + pass + + +class View3(View): + pass + + +nested = ([ + url(r'^view1/$', view1, name='view1'), + url(r'^view3/$', View3.as_view(), name='view3'), +], 'backend') + +urlpatterns = [ + url(r'^some/path/', include(nested, namespace='nested')), + url(r'^view2/$', view2, name='view2'), +] diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py index 074a573cd5..b0cb167bd8 100644 --- a/tests/urlpatterns_reverse/tests.py +++ b/tests/urlpatterns_reverse/tests.py @@ -428,6 +428,13 @@ class ResolverTests(unittest.TestCase): 'Wrong URL name. Expected "%s", got "%s".' % (e['name'], t.name) ) + def test_namespaced_view_detail(self): + resolver = get_resolver('urlpatterns_reverse.nested_urls') + self.assertTrue(resolver._is_callback('urlpatterns_reverse.nested_urls.view1')) + self.assertTrue(resolver._is_callback('urlpatterns_reverse.nested_urls.view2')) + self.assertTrue(resolver._is_callback('urlpatterns_reverse.nested_urls.View3')) + self.assertFalse(resolver._is_callback('urlpatterns_reverse.nested_urls.blub')) + @override_settings(ROOT_URLCONF='urlpatterns_reverse.reverse_lazy_urls') class ReverseLazyTest(TestCase):