Browse Source

Fixed #28766 -- Added ResolverMatch.route.

Co-Authored-By: Xavier Fernandez <xavier.fernandez@polyconseil.fr>
Benjamin Wohlwend 7 years ago
parent
commit
79c196cfb2

+ 16 - 4
django/urls/resolvers.py

@@ -28,11 +28,12 @@ from .utils import get_callable
 
 
 class ResolverMatch:
-    def __init__(self, func, args, kwargs, url_name=None, app_names=None, namespaces=None):
+    def __init__(self, func, args, kwargs, url_name=None, app_names=None, namespaces=None, route=None):
         self.func = func
         self.args = args
         self.kwargs = kwargs
         self.url_name = url_name
+        self.route = route
 
         # If a URLRegexResolver doesn't have a namespace or app_name, it passes
         # in an empty value.
@@ -55,9 +56,9 @@ class ResolverMatch:
         return (self.func, self.args, self.kwargs)[index]
 
     def __repr__(self):
-        return "ResolverMatch(func=%s, args=%s, kwargs=%s, url_name=%s, app_names=%s, namespaces=%s)" % (
+        return "ResolverMatch(func=%s, args=%s, kwargs=%s, url_name=%s, app_names=%s, namespaces=%s, route=%s)" % (
             self._func_path, self.args, self.kwargs, self.url_name,
-            self.app_names, self.namespaces,
+            self.app_names, self.namespaces, self.route,
         )
 
 
@@ -345,7 +346,7 @@ class URLPattern:
             new_path, args, kwargs = match
             # Pass any extra_kwargs as **kwargs.
             kwargs.update(self.default_args)
-            return ResolverMatch(self.callback, args, kwargs, self.pattern.name)
+            return ResolverMatch(self.callback, args, kwargs, self.pattern.name, route=str(self.pattern))
 
     @cached_property
     def lookup_str(self):
@@ -503,6 +504,15 @@ class URLResolver:
             self._populate()
         return self._app_dict[language_code]
 
+    @staticmethod
+    def _join_route(route1, route2):
+        """Join two routes, without the starting ^ in the second route."""
+        if not route1:
+            return route2
+        if route2.startswith('^'):
+            route2 = route2[1:]
+        return route1 + route2
+
     def _is_callback(self, name):
         if not self._populated:
             self._populate()
@@ -534,6 +544,7 @@ class URLResolver:
                         sub_match_args = sub_match.args
                         if not sub_match_dict:
                             sub_match_args = args + sub_match.args
+                        current_route = '' if isinstance(pattern, URLPattern) else str(pattern.pattern)
                         return ResolverMatch(
                             sub_match.func,
                             sub_match_args,
@@ -541,6 +552,7 @@ class URLResolver:
                             sub_match.url_name,
                             [self.app_name] + sub_match.app_names,
                             [self.namespace] + sub_match.namespaces,
+                            self._join_route(current_route, sub_match.route),
                         )
                     tried.append([pattern])
             raise Resolver404({'tried': tried, 'path': new_path})

+ 9 - 0
docs/ref/urlresolvers.txt

@@ -130,6 +130,15 @@ If the URL does not resolve, the function raises a
 
         The name of the URL pattern that matches the URL.
 
+    .. attribute:: ResolverMatch.route
+
+        .. versionadded:: 2.2
+
+        The route of the matching URL pattern.
+
+        For example, if ``path('users/<id>/', ...)`` is the matching pattern,
+        ``route`` will contain ``'users/<id>/'``.
+
     .. attribute:: ResolverMatch.app_name
 
         The application namespace for the URL pattern that matches the

+ 2 - 1
docs/releases/2.2.txt

@@ -268,7 +268,8 @@ Tests
 URLs
 ~~~~
 
-* ...
+* The new :attr:`.ResolverMatch.route` attribute stores the route of the
+  matching URL pattern.
 
 Validators
 ~~~~~~~~~~

+ 2 - 1
tests/urlpatterns/included_urls.py

@@ -1,7 +1,8 @@
-from django.urls import path
+from django.urls import include, path
 
 from . import views
 
 urlpatterns = [
     path('extra/<extra>/', views.empty_view, name='inner-extra'),
+    path('', include('urlpatterns.more_urls')),
 ]

+ 7 - 0
tests/urlpatterns/more_urls.py

@@ -0,0 +1,7 @@
+from django.urls import re_path
+
+from . import views
+
+urlpatterns = [
+    re_path(r'^more/(?P<extra>\w+)/$', views.empty_view, name='inner-more'),
+]

+ 3 - 1
tests/urlpatterns/path_urls.py

@@ -1,5 +1,5 @@
 from django.conf.urls import include
-from django.urls import path
+from django.urls import path, re_path
 
 from . import views
 
@@ -11,5 +11,7 @@ urlpatterns = [
     path('users/', views.empty_view, name='users'),
     path('users/<id>/', views.empty_view, name='user-with-id'),
     path('included_urls/', include('urlpatterns.included_urls')),
+    re_path(r'^regex/(?P<pk>[0-9]+)/$', views.empty_view, name='regex'),
+    path('', include('urlpatterns.more_urls')),
     path('<lang>/<path:url>/', views.empty_view, name='lang-and-path'),
 ]

+ 25 - 0
tests/urlpatterns/tests.py

@@ -26,23 +26,48 @@ class SimplifiedURLTests(SimpleTestCase):
         self.assertEqual(match.url_name, 'articles-2003')
         self.assertEqual(match.args, ())
         self.assertEqual(match.kwargs, {})
+        self.assertEqual(match.route, 'articles/2003/')
 
     def test_path_lookup_with_typed_parameters(self):
         match = resolve('/articles/2015/')
         self.assertEqual(match.url_name, 'articles-year')
         self.assertEqual(match.args, ())
         self.assertEqual(match.kwargs, {'year': 2015})
+        self.assertEqual(match.route, 'articles/<int:year>/')
 
     def test_path_lookup_with_multiple_paramaters(self):
         match = resolve('/articles/2015/04/12/')
         self.assertEqual(match.url_name, 'articles-year-month-day')
         self.assertEqual(match.args, ())
         self.assertEqual(match.kwargs, {'year': 2015, 'month': 4, 'day': 12})
+        self.assertEqual(match.route, 'articles/<int:year>/<int:month>/<int:day>/')
 
     def test_two_variable_at_start_of_path_pattern(self):
         match = resolve('/en/foo/')
         self.assertEqual(match.url_name, 'lang-and-path')
         self.assertEqual(match.kwargs, {'lang': 'en', 'url': 'foo'})
+        self.assertEqual(match.route, '<lang>/<path:url>/')
+
+    def test_re_path(self):
+        match = resolve('/regex/1/')
+        self.assertEqual(match.url_name, 'regex')
+        self.assertEqual(match.kwargs, {'pk': '1'})
+        self.assertEqual(match.route, '^regex/(?P<pk>[0-9]+)/$')
+
+    def test_path_lookup_with_inclusion(self):
+        match = resolve('/included_urls/extra/something/')
+        self.assertEqual(match.url_name, 'inner-extra')
+        self.assertEqual(match.route, 'included_urls/extra/<extra>/')
+
+    def test_path_lookup_with_empty_string_inclusion(self):
+        match = resolve('/more/99/')
+        self.assertEqual(match.url_name, 'inner-more')
+        self.assertEqual(match.route, r'^more/(?P<extra>\w+)/$')
+
+    def test_path_lookup_with_double_inclusion(self):
+        match = resolve('/included_urls/more/some_value/')
+        self.assertEqual(match.url_name, 'inner-more')
+        self.assertEqual(match.route, r'included_urls/more/(?P<extra>\w+)/$')
 
     def test_path_reverse_without_parameter(self):
         url = reverse('articles-2003')

+ 1 - 1
tests/urlpatterns_reverse/tests.py

@@ -1130,7 +1130,7 @@ class ResolverMatchTests(SimpleTestCase):
             repr(resolve('/no_kwargs/42/37/')),
             "ResolverMatch(func=urlpatterns_reverse.views.empty_view, "
             "args=('42', '37'), kwargs={}, url_name=no-kwargs, app_names=[], "
-            "namespaces=[])"
+            "namespaces=[], route=^no_kwargs/([0-9]+)/([0-9]+)/$)",
         )