diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py index 2d4610aac41..120e0396af9 100644 --- a/django/urls/resolvers.py +++ b/django/urls/resolvers.py @@ -158,8 +158,9 @@ class RegexPattern(CheckURLMixin): # If there are any named groups, use those as kwargs, ignoring # non-named groups. Otherwise, pass all non-named arguments as # positional arguments. - kwargs = {k: v for k, v in match.groupdict().items() if v is not None} + kwargs = match.groupdict() args = () if kwargs else match.groups() + kwargs = {k: v for k, v in kwargs.items() if v is not None} return path[match.end():], args, kwargs return None diff --git a/docs/releases/3.0.1.txt b/docs/releases/3.0.1.txt index 589ef40499d..cdfdc33e40b 100644 --- a/docs/releases/3.0.1.txt +++ b/docs/releases/3.0.1.txt @@ -13,3 +13,7 @@ Bugfixes inside Jupyter and other environments that force an async context, by adding and option to disable :ref:`async-safety` mechanism with ``DJANGO_ALLOW_ASYNC_UNSAFE`` environment variable (:ticket:`31056`). + +* Fixed a regression in Django 3.0 where ``RegexPattern``, used by + :func:`~django.urls.re_path`, returned positional arguments to be passed to + the view when all optional named groups were missing (:ticket:`31061`). diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 4283d6ebe16..5c1540b8091 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -53,7 +53,7 @@ algorithm the system follows to determine which Python code to execute: arguments: * An instance of :class:`~django.http.HttpRequest`. - * If the matched URL pattern returned no named groups, then the + * If the matched URL pattern contained no named groups, then the matches from the regular expression are provided as positional arguments. * The keyword arguments are made up of any named parts matched by the path expression, overridden by any arguments specified in the optional diff --git a/tests/urlpatterns/path_urls.py b/tests/urlpatterns/path_urls.py index b40801b39d3..afc15f30aff 100644 --- a/tests/urlpatterns/path_urls.py +++ b/tests/urlpatterns/path_urls.py @@ -12,6 +12,11 @@ urlpatterns = [ path('included_urls/', include('urlpatterns.included_urls')), re_path(r'^regex/(?P[0-9]+)/$', views.empty_view, name='regex'), re_path(r'^regex_optional/(?P\d+)/(?:(?P\d+)/)?', views.empty_view, name='regex_optional'), + re_path( + r'^regex_only_optional/(?:(?P\d+)/)?', + views.empty_view, + name='regex_only_optional', + ), path('', include('urlpatterns.more_urls')), path('//', views.empty_view, name='lang-and-path'), ] diff --git a/tests/urlpatterns/tests.py b/tests/urlpatterns/tests.py index 92c4e6399e1..b149e0d5126 100644 --- a/tests/urlpatterns/tests.py +++ b/tests/urlpatterns/tests.py @@ -68,6 +68,16 @@ class SimplifiedURLTests(SimpleTestCase): r'^regex_optional/(?P\d+)/(?:(?P\d+)/)?', ) + def test_re_path_with_missing_optional_parameter(self): + match = resolve('/regex_only_optional/') + self.assertEqual(match.url_name, 'regex_only_optional') + self.assertEqual(match.kwargs, {}) + self.assertEqual(match.args, ()) + self.assertEqual( + match.route, + r'^regex_only_optional/(?:(?P\d+)/)?', + ) + def test_path_lookup_with_inclusion(self): match = resolve('/included_urls/extra/something/') self.assertEqual(match.url_name, 'inner-extra')