From 3979aa81c2de8923d8b3def46855533881ab0c6a Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Thu, 13 Oct 2016 00:27:06 +0200 Subject: [PATCH] Consolidate requirements, remove django\<1.8 artifacts, update tests and reverse monkey_patching --- .coveragerc | 2 + Makefile | 2 +- pytest.ini | 1 + runtests.py | 111 ----------------------- setup.py | 50 +--------- testproject/testproject/settings/base.py | 10 +- tox.ini | 13 ++- wiki/models/__init__.py | 33 ++++--- wiki/plugins/attachments/urls.py | 6 +- wiki/tests/settings.py | 59 ++++++++++++ wiki/tests/test_urls.py | 8 +- wiki/tests/test_views.py | 2 +- wiki/tests/testdata/urls.py | 6 +- wiki/urls.py | 83 ++++++++--------- 14 files changed, 140 insertions(+), 246 deletions(-) create mode 100644 .coveragerc delete mode 100755 runtests.py create mode 100644 wiki/tests/settings.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..321e5ca0 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = */tests/* diff --git a/Makefile b/Makefile index 51f2e062..ca9fcbe0 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ lint: pep8 wiki test: - ./runtests.py + pytest test-all: tox diff --git a/pytest.ini b/pytest.ini index 5f38b960..ea8d3ba9 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,3 +3,4 @@ django_find_project = false python_files=test_*.py testpaths=wiki norecursedirs=testproject .svn _build tmp* dist *.egg-info +DJANGO_SETTINGS_MODULE=wiki.tests.settings diff --git a/runtests.py b/runtests.py deleted file mode 100755 index 9ba61f99..00000000 --- a/runtests.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python -from __future__ import absolute_import, unicode_literals - -import sys - -import django -import pytest -from django.conf import settings - -# Run py.tests -# Compatibility testing patches on the py-moneyed - -settings_dict = dict( - DEBUG=True, - AUTH_USER_MODEL='testdata.CustomUser', - WIKI_GROUP_MODEL='testdata.CustomGroup', - DATABASES={ - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - } - }, - SITE_ID=1, - ROOT_URLCONF='wiki.tests.testdata.urls', - INSTALLED_APPS=[ - 'wiki.tests.testdata', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.admin', - 'django.contrib.humanize', - 'django.contrib.sites', - 'django_nyt', - 'mptt', - 'sekizai', - 'sorl.thumbnail', - 'wiki', - 'wiki.plugins.attachments', - 'wiki.plugins.notifications', - 'wiki.plugins.images', - 'wiki.plugins.macros', - ] + (['south'] if django.VERSION < (1, 7) else []), - MIDDLEWARE_CLASSES=[ - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - ], - USE_TZ=True, - SOUTH_TESTS_MIGRATE=True, - SECRET_KEY='b^fv_)t39h%9p40)fnkfblo##jkr!$0)lkp6bpy!fi*f$4*92!', -) - -TEMPLATE_CONTEXT_PROCESSORS = [ - "django.contrib.auth.context_processors.auth", - "django.template.context_processors.debug", - "django.template.context_processors.i18n", - "django.template.context_processors.media", - "django.template.context_processors.request", - "django.template.context_processors.static", - "django.template.context_processors.tz", - "django.contrib.messages.context_processors.messages", - "sekizai.context_processors.sekizai", -] - -if django.VERSION < (1, 8): - settings_dict.update(dict( - TEMPLATE_CONTEXT_PROCESSORS=[p.replace('django.template.context_processors', - 'django.core.context_processors') - for p in TEMPLATE_CONTEXT_PROCESSORS] - )) -else: - settings_dict.update(dict( - TEMPLATES=[ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': TEMPLATE_CONTEXT_PROCESSORS - }, - }, - ] - )) - -settings.configure(**settings_dict) - - -# If you use South for migrations, uncomment this to monkeypatch -# syncdb to get migrations to run. -if django.VERSION < (1, 7): - from south.management.commands import patch_for_test_db_setup - patch_for_test_db_setup() - -argv = [sys.argv[0], "test", "--traceback"] - -# python setup.py test calls script with just 'test' -if len(sys.argv) == 1 or sys.argv[1] == 'test': - # Nothing following 'runtests.py': - if django.VERSION < (1, 6): - argv.extend(["wiki", "attachments"]) - else: - argv.extend(["wiki.tests", "wiki.plugins.attachments.tests"]) -else: - # Allow tests to be specified: - argv.extend(sys.argv[1:]) - - -failures = pytest.main() - -if failures: - sys.exit(failures) diff --git a/setup.py b/setup.py index 3beae7aa..f6c41a34 100755 --- a/setup.py +++ b/setup.py @@ -3,8 +3,6 @@ from __future__ import absolute_import, unicode_literals import os -from sys import version_info as PYTHON_VERSION - from setuptools import find_packages, setup from wiki import __version__ @@ -14,8 +12,6 @@ from wiki import __version__ # Used for the long_description. It's nice, because now 1) we have a top level # README file and 2) it's easier to type in the README file than to put a raw # string in below ... - - def get_path(fname): return os.path.join(os.path.dirname(__file__), fname) @@ -25,52 +21,16 @@ def read(fname): requirements = [ - "Django>=1.5", + "Django>=1.8", "Pillow", "django-nyt>=0.9.7.2,<1.0", "six", + "django-mptt>=0.8.6,<0.9", + "django-sekizai>=0.10", + "sorl-thumbnail>=12,<13", + "Markdown>=2.6,<2.7", ] - -# Requirements that depend on Django version: South and sorl-thumbnail -try: - from django import VERSION as DJANGO_VERSION -except ImportError: - # No Django so assuming that one will get installed, but we don't know which - # one. - # We will assume it's a very recent one and base the requirements on that... - requirements.append("sorl-thumbnail>=12") - # 0.6.1 broken: https://github.com/django-mptt/django-mptt/issues/316 - requirements.append("django-mptt>=0.8") - requirements.append("django-sekizai>=0.9") -else: - if DJANGO_VERSION < (1, 7): - requirements.append("South>=1.0.1") - requirements.append("django-mptt>=0.7.1,<0.8") - requirements.append("django-sekizai<0.9") - elif DJANGO_VERSION < (1, 8): - # Fixes - # AttributeError: 'URLPath' object has no attribute 'get_deferred_fields' - requirements.append("django-mptt>=0.7.1,<0.8") - requirements.append("django-sekizai>=0.9") - else: - # Latest django-mptt only works for Django 1.8+ - requirements.append("django-mptt>=0.8.6,<0.9") - requirements.append("django-sekizai>=0.9") - if DJANGO_VERSION < (1, 5): - # For Django 1.4, use sorl-thumbnail<11.12.1: - # https://github.com/mariocesar/sorl-thumbnail/issues/255 - requirements.append("sorl-thumbnail<11.12.1") - else: - requirements.append("sorl-thumbnail>=12,<13") - -if PYTHON_VERSION < (2, 7): - # For Python 2.6, use Markdown<2.5.0, see - # https://github.com/waylan/Python-Markdown/issues/349 - requirements.append("Markdown>2.2.0,<2.5.0") -else: - requirements.append("Markdown>2.2.0,<2.7") - packages = find_packages() diff --git a/testproject/testproject/settings/base.py b/testproject/testproject/settings/base.py index d6fb0b1c..e036bae8 100644 --- a/testproject/testproject/settings/base.py +++ b/testproject/testproject/settings/base.py @@ -13,7 +13,6 @@ from __future__ import unicode_literals import os -from django import VERSION from django.core.urlresolvers import reverse_lazy @@ -32,9 +31,6 @@ DEBUG = False ALLOWED_HOSTS = [] -# Application definition - - INSTALLED_APPS = [ 'django.contrib.humanize', 'django.contrib.auth', @@ -57,10 +53,8 @@ INSTALLED_APPS = [ 'wiki.plugins.notifications', 'mptt', ] -if VERSION < (1, 7): - INSTALLED_APPS.append('south') -else: - TEST_RUNNER = 'django.test.runner.DiscoverRunner' + +TEST_RUNNER = 'django.test.runner.DiscoverRunner' MIDDLEWARE_CLASSES = [ diff --git a/tox.ini b/tox.ini index 21f7d1aa..f2cd0663 100644 --- a/tox.ini +++ b/tox.ini @@ -5,24 +5,27 @@ envlist = {py27,py34,py35}-django{18,19,110} [testenv] commands = - {envbindir}/coverage run --source=wiki runtests.py + {envbindir}/pytest --cov=wiki --cov-config .coveragerc + +usedevelop = true deps = coverage pytest + pytest-django + pytest-cov Pillow==2.3.0 django-classy-tags==0.4 six>=1.9 mock>=2.0 - Markdown==2.6.5 + Markdown==2.6.7 django_nyt==0.9.8 django18: Django==1.8.2 django19: Django==1.9 - django110: Django==1.10 + django110: Django==1.10.2 django{18,19,110}: django-mptt==0.8.6 - django{18,19,110}: django-sekizai==0.9.0 + django{18,19,110}: django-sekizai==0.10.0 django{18,19,110}: sorl-thumbnail==12.3 - django{18,19,110}: pytest-django>=3 basepython = py27: python2.7 diff --git a/wiki/models/__init__.py b/wiki/models/__init__.py index 3b54b4d5..f2dac7f5 100644 --- a/wiki/models/__init__.py +++ b/wiki/models/__init__.py @@ -1,17 +1,16 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import unicode_literals -# -*- coding: utf-8 -*- -from django import VERSION from django.conf import settings as django_settings from django.core.exceptions import ImproperlyConfigured -import warnings -from six import string_types +from six import string_types, text_type # TODO: Don't use wildcards from .article import * # noqa from .pluginbase import * # noqa from .urlpath import * # noqa +from django.utils.functional import lazy # TODO: Should the below stuff be executed a more logical place? # Follow Django's default_settings.py / settings.py pattern and put these @@ -64,16 +63,6 @@ if 'django_notify' in django_settings.INSTALLED_APPS: raise ImproperlyConfigured( 'django-wiki: You need to change from django_notify to django_nyt in INSTALLED_APPS and your urlconfig.') -###################### -# Warnings -###################### - - -if VERSION < (1, 7): - if 'south' not in django_settings.INSTALLED_APPS: - warnings.warn( - "django-wiki: No south in your INSTALLED_APPS. This is highly discouraged.") - from django.core import urlresolvers # noqa @@ -108,4 +97,20 @@ def reverse(*args, **kwargs): return url # Now we redefine reverse method +reverse_lazy = lazy(reverse, text_type) urlresolvers.reverse = reverse +urlresolvers.reverse_lazy = reverse_lazy + +# Patch up other locations of the reverse function +try: + from django.urls import base + from django import urls + from django import shortcuts + base.reverse = reverse + base.reverse_lazy = reverse_lazy + urls.reverse = reverse + urls.reverse_lazy = reverse_lazy + shortcuts.reverse = reverse + urls.reverse_lazy = reverse_lazy +except ImportError: + pass diff --git a/wiki/plugins/attachments/urls.py b/wiki/plugins/attachments/urls.py index 6df12b89..81c2e393 100644 --- a/wiki/plugins/attachments/urls.py +++ b/wiki/plugins/attachments/urls.py @@ -1,7 +1,6 @@ from __future__ import absolute_import, unicode_literals -from django import VERSION as DJANGO_VERSION -from django.conf.urls import patterns, url +from django.conf.urls import url from wiki.plugins.attachments import views urlpatterns = [ @@ -33,6 +32,3 @@ urlpatterns = [ views.AttachmentChangeRevisionView.as_view(), name='attachments_revision_change'), ] - -if DJANGO_VERSION < (1, 8): - urlpatterns = patterns('', *urlpatterns) diff --git a/wiki/tests/settings.py b/wiki/tests/settings.py new file mode 100644 index 00000000..b930cf23 --- /dev/null +++ b/wiki/tests/settings.py @@ -0,0 +1,59 @@ +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + } +} + +DEBUG = True +AUTH_USER_MODEL = 'testdata.CustomUser' +WIKI_GROUP_MODEL = 'testdata.CustomGroup' +SITE_ID = 1 +ROOT_URLCONF = 'wiki.tests.testdata.urls' +INSTALLED_APPS = [ + 'wiki.tests.testdata', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.admin', + 'django.contrib.humanize', + 'django.contrib.sites', + 'django_nyt', + 'mptt', + 'sekizai', + 'sorl.thumbnail', + 'wiki', + 'wiki.plugins.attachments', + 'wiki.plugins.notifications', + 'wiki.plugins.images', + 'wiki.plugins.macros', +] +MIDDLEWARE_CLASSES = [ + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +] +USE_TZ = True +SECRET_KEY = 'b^fv_)t39h%9p40)fnkfblo##jkr!$0)lkp6bpy!fi*f$4*92!' +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.request", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", + "sekizai.context_processors.sekizai", + ] + }, + }, +] diff --git a/wiki/tests/test_urls.py b/wiki/tests/test_urls.py index 438fd079..bca00576 100644 --- a/wiki/tests/test_urls.py +++ b/wiki/tests/test_urls.py @@ -1,7 +1,7 @@ from __future__ import absolute_import, unicode_literals from django import VERSION as DJANGO_VERSION -from django.conf.urls import patterns, url +from django.conf.urls import url from django.contrib.auth import get_user_model from django.contrib.sites.models import Site from django.test.testcases import TestCase @@ -14,9 +14,6 @@ from wiki.urls import WikiURLPatterns User = get_user_model() - - - class WikiCustomUrlPatterns(WikiURLPatterns): def get_article_urls(self): @@ -41,9 +38,6 @@ urlpatterns = [ url(r'', get_wiki_pattern(url_config_class=WikiCustomUrlPatterns)) ] -if DJANGO_VERSION < (1, 8): - urlpatterns = patterns('', *urlpatterns) - @wiki_override_settings(WIKI_URL_CONFIG_CLASS='wiki.tests.test_models.WikiCustomUrlPatterns', ROOT_URLCONF='wiki.tests.test_urls') diff --git a/wiki/tests/test_views.py b/wiki/tests/test_views.py index c5fba5f1..1a80c2a4 100644 --- a/wiki/tests/test_views.py +++ b/wiki/tests/test_views.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, print_function, unicode_literals import pprint from django.contrib.auth import authenticate -from django.core.urlresolvers import reverse +from wiki.models import reverse from .base import ArticleWebTestBase, WebTestBase diff --git a/wiki/tests/testdata/urls.py b/wiki/tests/testdata/urls.py index 1fd4af0a..9724388d 100644 --- a/wiki/tests/testdata/urls.py +++ b/wiki/tests/testdata/urls.py @@ -1,8 +1,7 @@ from __future__ import absolute_import, unicode_literals -from django import VERSION as DJANGO_VERSION from django.conf import settings -from django.conf.urls import include, patterns, url +from django.conf.urls import include, url from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django_nyt.urls import get_pattern as get_notify_pattern @@ -28,6 +27,3 @@ urlpatterns += [ url(r'^notify/', get_notify_pattern()), url(r'', get_wiki_pattern()) ] - -if DJANGO_VERSION < (1, 8): - urlpatterns = patterns('', *urlpatterns) diff --git a/wiki/urls.py b/wiki/urls.py index c38f8342..ebbe3e0a 100644 --- a/wiki/urls.py +++ b/wiki/urls.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals -from django import VERSION as DJANGO_VERSION from django.conf.urls import include, url from wiki.conf import settings from wiki.core.plugins import registry @@ -62,20 +61,20 @@ class WikiURLPatterns(object): def get_root_urls(self): urlpatterns = [ - url('^$', + url(r'^$', self.article_view_class.as_view(), name='root', kwargs={'path': ''}), - url('^create-root/$', + url(r'^create-root/$', article.CreateRootView.as_view(), name='root_create'), - url('^missing-root/$', + url(r'^missing-root/$', article.MissingRootView.as_view(), name='root_missing'), - url('^_search/$', + url(r'^_search/$', get_class_from_str(self.search_view_class).as_view(), name='search'), - url('^_revision/diff/(?P\d+)/$', + url(r'^_revision/diff/(?P\d+)/$', self.article_diff_view, name='diff'), ] @@ -92,16 +91,16 @@ class WikiURLPatterns(object): def get_accounts_urls(self): if settings.ACCOUNT_HANDLING: urlpatterns = [ - url('^_accounts/sign-up/$', + url(r'^_accounts/sign-up/$', self.signup_view_class.as_view(), name='signup'), - url('^_accounts/logout/$', + url(r'^_accounts/logout/$', self.logout_view_class.as_view(), name='logout'), - url('^_accounts/login/$', + url(r'^_accounts/login/$', self.login_view_class.as_view(), name='login'), - url('^_accounts/settings/$', + url(r'^_accounts/settings/$', self.profile_update_view_class.as_view(), name='profile_update'), ] @@ -114,14 +113,14 @@ class WikiURLPatterns(object): # This one doesn't work because it don't know # where to redirect after... url( - '^_revision/change/(?P\d+)/(?P\d+)/$', + r'^_revision/change/(?P\d+)/(?P\d+)/$', self.revision_change_view_class.as_view(), name='change_revision'), - url('^_revision/preview/(?P\d+)/$', + url(r'^_revision/preview/(?P\d+)/$', self.article_preview_view_class.as_view(), name='preview_revision'), url( - '^_revision/merge/(?P\d+)/(?P\d+)/preview/$', + r'^_revision/merge/(?P\d+)/(?P\d+)/preview/$', self.revision_merge_view, name='merge_revision_preview', kwargs={ @@ -132,39 +131,39 @@ class WikiURLPatterns(object): def get_article_urls(self): urlpatterns = [ # Paths decided by article_ids - url('^(?P\d+)/$', + url(r'^(?P\d+)/$', self.article_view_class.as_view(), name='get'), - url('^(?P\d+)/delete/$', + url(r'^(?P\d+)/delete/$', self.article_delete_view_class.as_view(), name='delete'), - url('^(?P\d+)/deleted/$', + url(r'^(?P\d+)/deleted/$', self.article_deleted_view_class.as_view(), name='deleted'), - url('^(?P\d+)/edit/$', + url(r'^(?P\d+)/edit/$', self.article_edit_view_class.as_view(), name='edit'), - url('^(?P\d+)/preview/$', + url(r'^(?P\d+)/preview/$', self.article_preview_view_class.as_view(), name='preview'), - url('^(?P\d+)/history/$', + url(r'^(?P\d+)/history/$', self.article_history_view_class.as_view(), name='history'), - url('^(?P\d+)/settings/$', + url(r'^(?P\d+)/settings/$', self.article_settings_view_class.as_view(), name='settings'), - url('^(?P\d+)/source/$', + url(r'^(?P\d+)/source/$', self.article_source_view_class.as_view(), name='source'), url( - '^(?P\d+)/revision/change/(?P\d+)/$', + r'^(?P\d+)/revision/change/(?P\d+)/$', self.revision_change_view_class.as_view(), name='change_revision'), url( - '^(?P\d+)/revision/merge/(?P\d+)/$', + r'^(?P\d+)/revision/merge/(?P\d+)/$', self.revision_merge_view, name='merge_revision'), - url('^(?P\d+)/plugin/(?P\w+)/$', + url(r'^(?P\d+)/plugin/(?P\w+)/$', self.article_plugin_view_class.as_view(), name='plugin'), ] @@ -173,46 +172,46 @@ class WikiURLPatterns(object): def get_article_path_urls(self): urlpatterns = [ # Paths decided by URLs - url('^(?P.+/|)_create/$', + url(r'^(?P.+/|)_create/$', self.article_create_view_class.as_view(), name='create'), - url('^(?P.+/|)_delete/$', + url(r'^(?P.+/|)_delete/$', self.article_delete_view_class.as_view(), name='delete'), - url('^(?P.+/|)_deleted/$', + url(r'^(?P.+/|)_deleted/$', self.article_deleted_view_class.as_view(), name='deleted'), - url('^(?P.+/|)_edit/$', + url(r'^(?P.+/|)_edit/$', self.article_edit_view_class.as_view(), name='edit'), - url('^(?P.+/|)_preview/$', + url(r'^(?P.+/|)_preview/$', self.article_preview_view_class.as_view(), name='preview'), - url('^(?P.+/|)_history/$', + url(r'^(?P.+/|)_history/$', self.article_history_view_class.as_view(), name='history'), - url('^(?P.+/|)_dir/$', + url(r'^(?P.+/|)_dir/$', self.article_dir_view_class.as_view(), name='dir'), - url('^(?P.+/|)_settings/$', + url(r'^(?P.+/|)_settings/$', self.article_settings_view_class.as_view(), name='settings'), - url('^(?P.+/|)_source/$', + url(r'^(?P.+/|)_source/$', self.article_source_view_class.as_view(), name='source'), url( - '^(?P.+/|)_revision/change/(?P\d+)/$', + r'^(?P.+/|)_revision/change/(?P\d+)/$', self.revision_change_view_class.as_view(), name='change_revision'), url( - '^(?P.+/|)_revision/merge/(?P\d+)/$', + r'^(?P.+/|)_revision/merge/(?P\d+)/$', self.revision_merge_view, name='merge_revision'), - url('^(?P.+/|)_plugin/(?P\w+)/$', + url(r'^(?P.+/|)_plugin/(?P\w+)/$', self.article_plugin_view_class.as_view(), name='plugin'), # This should always go last! - url('^(?P.+/|)$', + url(r'^(?P.+/|)$', self.article_view_class.as_view(), name='get'), ] @@ -226,14 +225,14 @@ class WikiURLPatterns(object): if slug: article_urlpatterns = plugin.urlpatterns.get('article', []) urlpatterns += [ - url('^(?P\d+)/plugin/' + slug + '/', + url(r'^(?P\d+)/plugin/' + slug + '/', include(article_urlpatterns)), - url('^(?P.+/|)_plugin/' + slug + '/', + url(r'^(?P.+/|)_plugin/' + slug + '/', include(article_urlpatterns)), ] root_urlpatterns = plugin.urlpatterns.get('root', []) urlpatterns += [ - url('^_plugin/' + slug + '/', include(root_urlpatterns)), + url(r'^_plugin/' + slug + '/', include(root_urlpatterns)), ] return urlpatterns @@ -252,10 +251,6 @@ def get_pattern(app_name="wiki", namespace="wiki", url_config_class=None): url_config_class = get_class_from_str(url_config_classname) urlpatterns = url_config_class().get_urls() - if DJANGO_VERSION < (1, 8): - from django.conf.urls import patterns - urlpatterns = patterns('', *urlpatterns) - return urlpatterns, app_name, namespace -- 2.45.2