Skip to content

Commit

Permalink
Fixed #28766 -- Added ResolverMatch.route.
Browse files Browse the repository at this point in the history
Co-Authored-By: Xavier Fernandez <xavier.fernandez@polyconseil.fr>
  • Loading branch information
2 people authored and timgraham committed Dec 6, 2018
1 parent ad191d9 commit 79c196c
Show file tree
Hide file tree
Showing 8 changed files with 65 additions and 8 deletions.
20 changes: 16 additions & 4 deletions django/urls/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@


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.
Expand All @@ -55,9 +56,9 @@ def __getitem__(self, index):
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,
)


Expand Down Expand Up @@ -345,7 +346,7 @@ def resolve(self, path):
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):
Expand Down Expand Up @@ -503,6 +504,15 @@ def app_dict(self):
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()
Expand Down Expand Up @@ -534,13 +544,15 @@ def resolve(self, path):
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,
sub_match_dict,
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})
Expand Down
9 changes: 9 additions & 0 deletions docs/ref/urlresolvers.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion docs/releases/2.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ Tests
URLs
~~~~

* ...
* The new :attr:`.ResolverMatch.route` attribute stores the route of the
matching URL pattern.

Validators
~~~~~~~~~~
Expand Down
3 changes: 2 additions & 1 deletion tests/urlpatterns/included_urls.py
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions tests/urlpatterns/more_urls.py
Original file line number Diff line number Diff line change
@@ -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'),
]
4 changes: 3 additions & 1 deletion tests/urlpatterns/path_urls.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -11,5 +11,7 @@
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 changes: 25 additions & 0 deletions tests/urlpatterns/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,48 @@ def test_path_lookup_without_parameters(self):
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')
Expand Down
2 changes: 1 addition & 1 deletion tests/urlpatterns_reverse/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1130,7 +1130,7 @@ def test_repr(self):
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]+)/$)",
)


Expand Down

0 comments on commit 79c196c

Please sign in to comment.