diff --git a/django/utils/regex_helper.py b/django/utils/regex_helper.py index 622d8227597..d046b71d96e 100644 --- a/django/utils/regex_helper.py +++ b/django/utils/regex_helper.py @@ -7,7 +7,10 @@ should be good enough for a large class of URLS, however. """ from __future__ import unicode_literals +import warnings + from django.utils import six +from django.utils.deprecation import RemovedInDjango21Warning from django.utils.six.moves import zip # Mapping of an escape character to a representative of that class. So, e.g., @@ -59,9 +62,7 @@ def normalize(pattern): (3) Select the first (essentially an arbitrary) element from any character class. Select an arbitrary character for any unordered class (e.g. '.' or '\w') in the pattern. - (4) Ignore comments, look-ahead and look-behind assertions, and any of the - reg-exp flags that won't change what we construct ("iLmsu"). "(?x)" is - an error, however. + (4) Ignore look-ahead and look-behind assertions. (5) Raise an error on any disjunctive ('|') constructs. Django's URLs for forward resolving are either all positional arguments or @@ -128,10 +129,16 @@ def normalize(pattern): walk_to_end(ch, pattern_iter) else: ch, escaped = next(pattern_iter) - if ch in "iLmsu#!=<": + if ch in '!=<': # All of these are ignorable. Walk to the end of the # group. walk_to_end(ch, pattern_iter) + elif ch in 'iLmsu#': + warnings.warn( + 'Using (?%s) in url() patterns is deprecated.' % ch, + RemovedInDjango21Warning + ) + walk_to_end(ch, pattern_iter) elif ch == ':': # Non-capturing group non_capturing_groups.append(len(result)) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 29f4479c5a2..c1a1c41cd6a 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -48,6 +48,9 @@ details on these changes. * The ``Model._meta.has_auto_field`` attribute will be removed. +* Support for regular expression groups with ``iLmsu#`` in ``url()`` will be + removed. + .. _deprecation-removed-in-2.0: 2.0 diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index 24247f7d75c..48849f3b4d9 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -747,3 +747,10 @@ Miscellaneous * ``Model._meta.has_auto_field`` is deprecated in favor of checking if ``Model._meta.auto_field is not None``. + +* Using regular expression groups with ``iLmsu#`` in ``url()`` is deprecated. + The only group that's useful is ``(?i)`` for case-insensitive URLs, however, + case-insensitive URLs aren't a good practice because they create multiple + entries for search engines, for example. An alternative solution could be to + create a :data:`~django.conf.urls.handler404` that looks for uppercase + characters in the URL and redirects to a lowercase equivalent. diff --git a/tests/urlpatterns_reverse/test_deprecated.py b/tests/urlpatterns_reverse/test_deprecated.py new file mode 100644 index 00000000000..c918dd5a179 --- /dev/null +++ b/tests/urlpatterns_reverse/test_deprecated.py @@ -0,0 +1,33 @@ +import warnings + +from django.conf.urls import url +from django.test import SimpleTestCase, override_settings +from django.urls import reverse + +from .views import empty_view + +urlpatterns = [ + url(r'^(?i)CaseInsensitive/(\w+)', empty_view, name="insensitive"), + url(r'^(?i)test/2/?$', empty_view, name="test2"), +] + + +@override_settings(ROOT_URLCONF='urlpatterns_reverse.test_deprecated') +class URLPatternReverse(SimpleTestCase): + + def test_urlpattern_reverse(self): + test_data = ( + ('insensitive', '/CaseInsensitive/fred', ['fred'], {}), + ('test2', '/test/2', [], {}), + ) + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter('always') + warnings.filterwarnings( + 'ignore', 'Flags not at the start', + DeprecationWarning, module='django.urls.resolvers' + ) + for i, (name, expected, args, kwargs) in enumerate(test_data): + got = reverse(name, args=args, kwargs=kwargs) + self.assertEqual(got, expected) + msg = str(warns[i].message) + self.assertEqual(msg, 'Using (?i) in url() patterns is deprecated.') diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py index 4b31e382e1a..b02898e3458 100644 --- a/tests/urlpatterns_reverse/tests.py +++ b/tests/urlpatterns_reverse/tests.py @@ -198,9 +198,7 @@ test_data = ( ('repeats', '/repeats/a/', [], {}), ('repeats2', '/repeats/aa/', [], {}), ('repeats3', '/repeats/aa/', [], {}), - ('insensitive', '/CaseInsensitive/fred', ['fred'], {}), ('test', '/test/1', [], {}), - ('test2', '/test/2', [], {}), ('inner-nothing', '/outer/42/', [], {'outer': '42'}), ('inner-nothing', '/outer/42/', ['42'], {}), ('inner-nothing', NoReverseMatch, ['foo'], {}), diff --git a/tests/urlpatterns_reverse/urls.py b/tests/urlpatterns_reverse/urls.py index 35e2ff707c8..731c97146b3 100644 --- a/tests/urlpatterns_reverse/urls.py +++ b/tests/urlpatterns_reverse/urls.py @@ -49,9 +49,7 @@ urlpatterns = [ url(r'^repeats/a{1,2}/$', empty_view, name="repeats"), url(r'^repeats/a{2,4}/$', empty_view, name="repeats2"), url(r'^repeats/a{2}/$', empty_view, name="repeats3"), - url(r'^(?i)CaseInsensitive/(\w+)', empty_view, name="insensitive"), url(r'^test/1/?', empty_view, name="test"), - url(r'^(?i)test/2/?$', empty_view, name="test2"), url(r'^outer/(?P[0-9]+)/', include('urlpatterns_reverse.included_urls')), url(r'^outer-no-kwargs/([0-9]+)/', include('urlpatterns_reverse.included_no_kwargs_urls')), url('', include('urlpatterns_reverse.extra_urls')), diff --git a/tests/utils_tests/test_regex_helper.py b/tests/utils_tests/test_regex_helper.py index 055da89ee09..f91fd1cec7e 100644 --- a/tests/utils_tests/test_regex_helper.py +++ b/tests/utils_tests/test_regex_helper.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import unittest +import warnings from django.utils import regex_helper @@ -27,8 +28,12 @@ class NormalizeTests(unittest.TestCase): def test_group_ignored(self): pattern = r"(?i)(?L)(?m)(?s)(?u)(?#)" expected = [('', [])] - result = regex_helper.normalize(pattern) + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter('always') + result = regex_helper.normalize(pattern) self.assertEqual(result, expected) + for i, char in enumerate('iLmsu#'): + self.assertEqual(str(warns[i].message), 'Using (?%s) in url() patterns is deprecated.' % char) def test_group_noncapturing(self): pattern = r"(?:non-capturing)"