diff --git a/.pylintrc b/.pylintrc index 210b081..b70bbbb 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,7 +1,7 @@ [MASTER] persistent=no -#load-plugins=pylint_django +load-plugins=pylint_django [MESSAGES CONTROL] diff --git a/setup.py b/setup.py index dadfe32..d7c4b44 100644 --- a/setup.py +++ b/setup.py @@ -79,5 +79,6 @@ setup( }, install_requires=[ 'django', + 'mock', ], ) diff --git a/src/django_deploy/__init__.py b/src/django_deploy/__init__.py index c28a133..bb17839 100644 --- a/src/django_deploy/__init__.py +++ b/src/django_deploy/__init__.py @@ -1 +1,2 @@ +from .config import get_installed_apps, get_urlpatterns from .main import main diff --git a/src/django_deploy/config.py b/src/django_deploy/config.py index 55970af..53291ec 100644 --- a/src/django_deploy/config.py +++ b/src/django_deploy/config.py @@ -1 +1,102 @@ -DJANGO_SETTINGS_MODULE_NAME = 'main' +try: + from collections.abc import MutableMapping +except ImportError: # pragma: no cover + from collections import MutableMapping +import json +import os + +from django.conf.urls import url, include + +DJANGO_SETTINGS_DIR = 'main' + + +class _BaseDict(MutableMapping): + def __init__(self, *args, **kwargs): + super(_BaseDict, self).__init__(*args, **kwargs) + self._store = dict() + self.update(dict(*args, **kwargs)) + + def __getitem__(self, key): + return self._store[self.__keytransform__(key)] + + def __setitem__(self, key, value): + self._store[self.__keytransform__(key)] = value + + def __delitem__(self, key): + del self._store[self.__keytransform__(key)] + + def __iter__(self): + return iter(self._store) + + def __len__(self): + return len(self._store) + + @staticmethod + def __keytransform__(key): + return key + + +class DeployedAppsConfig(_BaseDict): # pylint: disable=too-many-ancestors + def load(self, path=None): + if path is None: + path = self._json_file + if os.path.exists(path): + with open(path, 'r') as f: + self._store = json.load(f) + else: + self._store = dict() + + def write(self, path=None): + if path is None: + path = self._json_file + with open(path, 'w') as f: + json.dump(self._store, f, indent=4) + + def __init__(self, project_dir=None, settings_dir=None): + assert (project_dir or settings_dir), 'DeployedAppsConfig(): ' \ + 'Either keyword argument project_dir or settings_dir' \ + ' must be set.' + assert (not (project_dir and settings_dir)), 'DeployedAppsConfig(): ' \ + 'Keyword arguments project_dir and settings_dir' \ + ' are mutually exclusive.' + super(DeployedAppsConfig, self).__init__() + if project_dir is not None: + settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_DIR) + self._json_file = os.path.join(settings_dir, 'django_deploy.json') + self.load() + + +def get_installed_apps(file_path, installed_apps): + settings_dir = os.path.dirname(os.path.abspath(file_path)) + config = DeployedAppsConfig(settings_dir=settings_dir) + app_list = installed_apps[:] + for wanting_app in config: + wanted_apps = config[wanting_app].get('INSTALLED_APPS', []) + for wanted_app in wanted_apps: + if wanted_app not in app_list: + app_list.append(wanted_app) + return app_list + + +def get_urlpatterns(file_path, urlpatterns): + settings_dir = os.path.dirname(os.path.abspath(file_path)) + config = DeployedAppsConfig(settings_dir=settings_dir) + urls_list = urlpatterns[:] + patterns = [] + for url_obj in urls_list: + # Django 1 vs Django 2 + if url_obj.__class__.__name__.startswith('Regex'): # pragma: no cover + patterns.append(url_obj.regex.pattern) + else: # pragma: no cover + patterns.append(str(url_obj.pattern)) + for wanting_app in config: + wanted_urls = config[wanting_app].get('urlpatterns', []) + for wanted_url in wanted_urls: + pattern = wanted_url['pattern'] + if pattern in patterns: + continue + if wanted_url['type'] == 'include': + url_obj = url(pattern, include(wanted_url['module'])) + urls_list.append(url_obj) + patterns.append(pattern) + return urls_list diff --git a/src/django_deploy/django_settings.py b/src/django_deploy/django_settings.py index 81fcbb1..3b21247 100644 --- a/src/django_deploy/django_settings.py +++ b/src/django_deploy/django_settings.py @@ -1 +1,4 @@ # ADD_INSTALLED_APPS = ['django_deploy'] +# ADD_URLPATTERNS = [ +# {'type': 'include', 'pattern': '', 'module': 'django_deploy.urls'}, +# ] diff --git a/src/django_deploy/exceptions.py b/src/django_deploy/exceptions.py new file mode 100644 index 0000000..229a4c1 --- /dev/null +++ b/src/django_deploy/exceptions.py @@ -0,0 +1,8 @@ +class FatalError(Exception): + def __init__(self, *args, **kwargs): + self._exitval = kwargs.pop('exitval', None) + super(FatalError, self).__init__(*args, **kwargs) + + @property + def exitval(self): + return self._exitval diff --git a/src/django_deploy/program.py b/src/django_deploy/program.py index efb5d9f..55043e8 100644 --- a/src/django_deploy/program.py +++ b/src/django_deploy/program.py @@ -1,10 +1,10 @@ import argparse -import datetime import importlib import os import sys -from .config import DJANGO_SETTINGS_MODULE_NAME +from .config import DJANGO_SETTINGS_DIR, DeployedAppsConfig +from .exceptions import FatalError from .version import VERSION @@ -23,6 +23,10 @@ class Program(object): # pylint: disable=too-few-public-methods action='store_true', dest='create', help='Create the django project directory') + parser.add_argument('-e', '--enable', + action='store_true', dest='enable', + help='Enable django_deploy in an existing django project directory') + parser.add_argument('project_dir', metavar='PATH', help='The directory, where the django project is or will be installed.') @@ -37,70 +41,93 @@ class Program(object): # pylint: disable=too-few-public-methods return self._argparser.parse_args(argv) @staticmethod - def _append_to_settings(project_dir, code, comment): - settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_MODULE_NAME) + def _append_to_pythonfile(path, text): + py2_cache = path + 'c' + with open(path, 'a') as f: + f.write(text) + if os.path.isfile(py2_cache): + os.unlink(py2_cache) # pragma: no cover + + def _append_to_settings(self, project_dir, code, comment): + settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_DIR) settings_file = os.path.join(settings_dir, 'settings.py') - settings_file_py2cache = settings_file + 'c' text = '\n' + comment + '\n' + code + '\n' - with open(settings_file, 'a') as f: - f.write(text) - if os.path.isfile(settings_file_py2cache): - os.unlink(settings_file_py2cache) # pragma: no cover + return self._append_to_pythonfile(settings_file, text) + + def _append_to_urlconf(self, project_dir, code, comment): + settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_DIR) + settings_file = os.path.join(settings_dir, 'urls.py') + + text = '\n' + comment + '\n' + code + '\n' + return self._append_to_pythonfile(settings_file, text) @staticmethod def _install_django_files(project_dir, overwrite=False): - if os.path.exists(project_dir): - if not overwrite: - sys.stderr.write('directory already exists: {}\n'.format(project_dir)) - return os.EX_NOPERM - else: + if not os.path.exists(project_dir): os.makedirs(project_dir) - settings_module = DJANGO_SETTINGS_MODULE_NAME - cmd = 'django-admin startproject {name} {path}'.format(name=settings_module, + settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_DIR) + if os.path.exists(settings_dir) and not overwrite: + sys.stderr.write('directory already exists: {}\n'.format(settings_dir)) + raise FatalError(exitval=os.EX_NOPERM) + + cmd = 'django-admin startproject {name} {path}'.format(name=DJANGO_SETTINGS_DIR, path=project_dir) sys.stdout.write('Installing django files to {path}\n'.format(path=project_dir)) exitval = os.system(cmd) - return exitval + if exitval != os.EX_OK: # pragma: no cover + raise FatalError(exitval=exitval) - def _add_installed_apps_from_app(self, project_dir, app): - settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_MODULE_NAME) + @staticmethod + def _install_django_deploy_files(project_dir, overwrite=False): + json_file = os.path.join(project_dir, DJANGO_SETTINGS_DIR, 'django_deploy.json') + if os.path.exists(json_file) and not overwrite: + sys.stderr.write('file already exists: {}\n'.format(json_file)) + raise FatalError(exitval=os.EX_NOPERM) + config = DeployedAppsConfig(project_dir=project_dir) + config.write() + + def _enable_django_deploy(self, project_dir): + settings_code = '' + settings_code += 'from django_deploy import get_installed_apps\n' + settings_code += 'INSTALLED_APPS = get_installed_apps(__file__, INSTALLED_APPS)\n' + settings_comment = '# django-deploy' + self._append_to_settings(project_dir, settings_code, settings_comment) + urlconf_code = '' + urlconf_code += 'from django_deploy import get_urlpatterns\n' + urlconf_code += 'urlpatterns = get_urlpatterns(__file__, urlpatterns)\n' + urlconf_comment = '# django-deploy' + self._append_to_urlconf(project_dir, urlconf_code, urlconf_comment) + + @staticmethod + def _add_installed_apps_from_app(project_dir, app): settings_from_app = importlib.import_module('{}.django_settings'.format(app)) - if hasattr(settings_from_app, 'ADD_INSTALLED_APPS'): - add_apps = settings_from_app.ADD_INSTALLED_APPS - else: - add_apps = [] + wanted_apps = getattr(settings_from_app, 'ADD_INSTALLED_APPS', []) - if not add_apps: - sys.stdout.write('{}: do not care about INSTALLED_APPS\n'.format(app)) - return os.EX_OK + config = DeployedAppsConfig(project_dir=project_dir) + if app not in config: + config[app] = {} - sys.path.insert(0, settings_dir) - settings = importlib.import_module('settings') - sys.path.pop(0) + config[app]['INSTALLED_APPS'] = wanted_apps + config.write() - missing_apps = [] - for add_app in add_apps: - if add_app not in settings.INSTALLED_APPS: - missing_apps.append(add_app) + @staticmethod + def _add_urlpatterns_from_app(project_dir, app): + settings_from_app = importlib.import_module('{}.django_settings'.format(app)) + wanted_urls = getattr(settings_from_app, 'ADD_URLPATTERNS', []) - if missing_apps: - sys.stdout.write('{app}: adding {apps} to INSTALLED_APPS\n'.format(app=app, apps=missing_apps)) - code = 'INSTALLED_APPS += [\n' - for add_app in missing_apps: - code += ' \'{}\',\n'.format(add_app) - code += ']\n' - timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - comment = '### {app}: added apps with django-deploy at {timestamp}'.format(app=app, - timestamp=timestamp) + config = DeployedAppsConfig(project_dir=project_dir) + if app not in config: # pragma: no cover + config[app] = {} - self._append_to_settings(project_dir, code, comment) - else: - sys.stdout.write('{app}: INSTALLED_APPS is fine\n'.format(app=app)) + config[app]['urlpatterns'] = wanted_urls + config.write() - return os.EX_OK + def _merge_app(self, project_dir, app): + self._add_installed_apps_from_app(project_dir, app) + self._add_urlpatterns_from_app(project_dir, app) def __init__(self): self._argparser = argparse.ArgumentParser() @@ -110,13 +137,15 @@ class Program(object): # pylint: disable=too-few-public-methods argv = kwargs.get('argv', None) cmd_args = self._parse_args(argv) exitval = os.EX_OK - if cmd_args.create: - exitval = self._install_django_files(cmd_args.project_dir) - if exitval != os.EX_OK: - return exitval - if cmd_args.merge_apps: - for app in cmd_args.merge_apps: - exitval = self._add_installed_apps_from_app(cmd_args.project_dir, app) - if exitval != os.EX_OK: - return exitval + try: + if cmd_args.create: + self._install_django_files(cmd_args.project_dir) + if cmd_args.create or cmd_args.enable: + self._install_django_deploy_files(cmd_args.project_dir) + self._enable_django_deploy(cmd_args.project_dir) + if cmd_args.merge_apps: + for app in cmd_args.merge_apps: + self._merge_app(cmd_args.project_dir, app) + except FatalError as e: + exitval = e.exitval return exitval diff --git a/src/django_deploy/tests/django_settings.py b/src/django_deploy/tests/django_settings.py deleted file mode 100644 index d4f2f21..0000000 --- a/src/django_deploy/tests/django_settings.py +++ /dev/null @@ -1 +0,0 @@ -ADD_INSTALLED_APPS = ['fake_app1', 'fake_app2'] diff --git a/src/django_deploy/tests/fake_app1/__init__.py b/src/django_deploy/tests/fake_app1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/django_deploy/tests/fake_app1/django_settings.py b/src/django_deploy/tests/fake_app1/django_settings.py new file mode 100644 index 0000000..fffc431 --- /dev/null +++ b/src/django_deploy/tests/fake_app1/django_settings.py @@ -0,0 +1,5 @@ +ADD_INSTALLED_APPS = ['django_deploy.tests.fake_app1'] +ADD_URLPATTERNS = [ + {'type': 'include', 'pattern': '^fake1/', 'module': 'django_deploy.tests.fake_app1.urls'}, + {'type': 'invalid', 'pattern': '^invalid/'}, +] diff --git a/src/django_deploy/tests/fake_app1/urls.py b/src/django_deploy/tests/fake_app1/urls.py new file mode 100644 index 0000000..0d2edeb --- /dev/null +++ b/src/django_deploy/tests/fake_app1/urls.py @@ -0,0 +1,3 @@ +# pylint: skip-file +app_name = 'fake1' +urlpatterns = [] diff --git a/src/django_deploy/tests/fake_app2/__init__.py b/src/django_deploy/tests/fake_app2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/django_deploy/tests/fake_app2/django_settings.py b/src/django_deploy/tests/fake_app2/django_settings.py new file mode 100644 index 0000000..5bbce39 --- /dev/null +++ b/src/django_deploy/tests/fake_app2/django_settings.py @@ -0,0 +1,5 @@ +ADD_INSTALLED_APPS = ['django_deploy.tests.fake_app1', 'django_deploy.tests.fake_app2'] +ADD_URLPATTERNS = [ + {'type': 'include', 'pattern': '^fake1/', 'module': 'django_deploy.tests.fake_app1.urls'}, + {'type': 'include', 'pattern': '^fake2/', 'module': 'django_deploy.tests.fake_app2.urls'}, +] diff --git a/src/django_deploy/tests/fake_app2/urls.py b/src/django_deploy/tests/fake_app2/urls.py new file mode 100644 index 0000000..3197708 --- /dev/null +++ b/src/django_deploy/tests/fake_app2/urls.py @@ -0,0 +1,3 @@ +# pylint: skip-file +app_name = 'fake2' +urlpatterns = [] diff --git a/src/django_deploy/tests/test_config.py b/src/django_deploy/tests/test_config.py new file mode 100644 index 0000000..8063bc4 --- /dev/null +++ b/src/django_deploy/tests/test_config.py @@ -0,0 +1,54 @@ +import unittest + +from django.conf.urls import url, include + +from ..config import get_installed_apps, get_urlpatterns, DeployedAppsConfig + + +class FunctionsTestCase(unittest.TestCase): + def test_get_installed_apps(self): + test_data_sets = [ + (__file__, ['fake_app1', 'fake_app2']), + ] + + for file_path, installed_apps in test_data_sets: + result = get_installed_apps(file_path, installed_apps) + self.assertEqual(installed_apps, result) + + def test_get_urlpatterns(self): + test_data_sets = [ + ( + __file__, + [ + url('/app1', include('django_deploy.tests.fake_app1.urls')), + url('/app2', include('django_deploy.tests.fake_app2.urls')), + ], + ), + ] + + for file_path, urlpatterns in test_data_sets: + result = get_urlpatterns(file_path, urlpatterns) + self.assertEqual(urlpatterns, result) + + +class DeployedAppsConfigTestCase(unittest.TestCase): + def test_init_parameters(self): + with self.assertRaises(AssertionError): + DeployedAppsConfig() + with self.assertRaises(AssertionError): + DeployedAppsConfig(project_dir='.', settings_dir='.') + + def test_len(self): + c = DeployedAppsConfig(settings_dir='.') + c['a'] = 'a' + c['b'] = 'b' + c['c'] = 'c' + self.assertEqual(3, len(c)) + + def test_del(self): + c = DeployedAppsConfig(settings_dir='.') + c['a'] = 'a' + c['b'] = 'b' + c['c'] = 'c' + del c['b'] + self.assertEqual(2, len(c)) diff --git a/src/django_deploy/tests/test_program.py b/src/django_deploy/tests/test_program.py index ff5196e..ae71d0f 100644 --- a/src/django_deploy/tests/test_program.py +++ b/src/django_deploy/tests/test_program.py @@ -2,47 +2,104 @@ import importlib import os import sys import unittest +import mock import pytest -from ..config import DJANGO_SETTINGS_MODULE_NAME +from ..config import DJANGO_SETTINGS_DIR from ..program import Program -class MainTestCase(unittest.TestCase): +class ProgramTestCase(unittest.TestCase): @pytest.fixture(autouse=True) def tmpdir(self, tmpdir): # pylint: disable=method-hidden self.tmpdir = tmpdir + def _assert_django_project(self, project_dir): + self.assertTrue(os.path.isdir(project_dir), 'no directory: {}'.format(project_dir)) + settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_DIR) + self.assertTrue(os.path.isdir(settings_dir), 'no directory: {}'.format(settings_dir)) + settings_file = os.path.join(settings_dir, 'settings.py') + self.assertTrue(os.path.isfile(settings_file), 'no file: {}'.format(settings_file)) + manage_script = os.path.join(project_dir, 'manage.py') + self.assertTrue(os.path.isfile(manage_script), 'no file: {}'.format(manage_script)) + + def _assert_django_deploy_project(self, project_dir): + settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_DIR) + settings_file = os.path.join(settings_dir, 'settings.py') + urlconf_file = os.path.join(settings_dir, 'urls.py') + django_deploy_json = os.path.join(settings_dir, 'django_deploy.json') + + self.assertTrue(os.path.isfile(django_deploy_json), 'no file: {}'.format(django_deploy_json)) + + with open(settings_file, 'r') as f: + needle = 'from django_deploy import get_installed_apps' + haystack = f.read() + self.assertIn(needle, haystack) + + with open(urlconf_file, 'r') as f: + needle = 'from django_deploy import get_urlpatterns' + haystack = f.read() + self.assertIn(needle, haystack) + def setUp(self): self._program = Program() + self._project_dir = os.path.join(str(self.tmpdir), 'django') + self._program(argv=['--create', self._project_dir]) - def test_create(self): - tmpdir = self.tmpdir - project_dir = os.path.join(str(tmpdir), 'env', 'django') - + def test_create_new_dir(self): + project_dir = os.path.join(str(self.tmpdir), 'new_dir') exitval = self._program(argv=['-c', project_dir]) self.assertEqual(os.EX_OK, exitval, 'program() does not return os.EX_OK') - self.assertTrue(os.path.isdir(project_dir), 'project directory was not created') - settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_MODULE_NAME) - self.assertTrue(os.path.isdir(settings_dir), 'settings directory was not created') - settings_file = os.path.join(settings_dir, 'settings.py') - self.assertTrue(os.path.isfile(settings_file), 'settings.py was not created') - manage_script = os.path.join(project_dir, 'manage.py') - self.assertTrue(os.path.isfile(manage_script), 'manage.py was not created') + self._assert_django_project(project_dir) + self._assert_django_deploy_project(project_dir) - def test_create_dont_overwrite(self): - tmpdir = self.tmpdir - project_dir = os.path.join(str(tmpdir), 'env', 'django') + def test_create_existing_empty_dir(self): + project_dir = os.path.join(str(self.tmpdir), 'empty_dir') + os.makedirs(project_dir) + exitval = self._program(argv=['-c', project_dir]) - self._program(argv=['--create', project_dir]) - exitval = self._program(argv=['--create', project_dir]) + self.assertEqual(os.EX_OK, exitval, 'program() does not return os.EX_OK') + self._assert_django_project(project_dir) + self._assert_django_deploy_project(project_dir) - self.assertEqual(os.EX_NOPERM, exitval, 'second call to program() does not exit with os.EX_NOPERM') + def test_create_existing_project_dir(self): + exitval = self._program(argv=['-c', self._project_dir]) + self.assertEqual(os.EX_NOPERM, exitval, 'program() does not return os.EX_NOPERM' + ' when project directory is not empty') + + def test_enable_django_deploy(self): + project_dir = os.path.join(str(self.tmpdir), 'pure_django') + os.makedirs(project_dir) + cmd = 'django-admin startproject {name} {path}'.format(name=DJANGO_SETTINGS_DIR, + path=project_dir) + os.system(cmd) + self._program(argv=['--enable', project_dir]) + self._assert_django_deploy_project(project_dir) + + def test_enable_django_deploy_twice(self): + exitval = self._program(argv=['-e', self._project_dir]) + self.assertEqual(os.EX_NOPERM, exitval, 'program() does not return os.EX_NOPERM' + ' when django_deploy is already enabled') def test_merge_installed_apps(self): - tmpdir = self.tmpdir - project_dir = os.path.join(str(tmpdir), 'env', 'django') + project_dir = self._project_dir + + argv = [ + '-a', 'django_deploy', + '-a', 'django_deploy.tests.fake_app1', + '-a', 'django_deploy.tests.fake_app2', + project_dir] + self._program(argv=argv) + + settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_DIR) + sys.path.insert(0, settings_dir) + settings = importlib.import_module('settings') + if sys.version_info.major == 2: # pragma: no cover + reload(settings) # pylint: disable=undefined-variable + else: # pragma: no cover + importlib.reload(settings) # pylint: disable=no-member + sys.path.pop(0) expected_installed_apps = [ 'django.contrib.admin', @@ -51,22 +108,66 @@ class MainTestCase(unittest.TestCase): 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'fake_app1', - 'fake_app2', + 'django_deploy.tests.fake_app1', + 'django_deploy.tests.fake_app2', ] - argv = ['-c', project_dir] - self._program(argv=argv) - - argv = ['-a', 'django_deploy', '-a', 'django_deploy.tests', project_dir] - self._program(argv=argv) - - settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_MODULE_NAME) - sys.path.insert(0, settings_dir) - settings = importlib.import_module('settings') - if sys.version_info.major == 2: # pragma: no cover - reload(settings) # pylint: disable=undefined-variable - else: # pragma: no cover - importlib.reload(settings) # pylint: disable=no-member - sys.path.pop(0) self.assertListEqual(expected_installed_apps, settings.INSTALLED_APPS) + + def test_merge_root_urlconf(self): + project_dir = self._project_dir + + argv = [ + '-a', 'django_deploy', + '-a', 'django_deploy.tests.fake_app1', + '-a', 'django_deploy.tests.fake_app2', + project_dir] + self._program(argv=argv) + + sys.modules['django.contrib'] = mock.MagicMock(name='django.contrib') + + settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_DIR) + sys.path.insert(0, settings_dir) + root_urlconf = importlib.import_module('urls') + if sys.version_info.major == 2: # pragma: no cover + reload(root_urlconf) # pylint: disable=undefined-variable + else: # pragma: no cover + importlib.reload(root_urlconf) # pylint: disable=no-member + sys.path.pop(0) + + # Django 2 vs Django 1 + if hasattr(root_urlconf, 'path'): # pragma: no cover + expected_urlpatterns = [ + ('URLPattern', 'admin/', 'django.contrib.admin.site.urls'), + ] + else: # pragma: no cover + expected_urlpatterns = [ + ('URLPattern', '^admin/', 'django.contrib.admin.site.urls'), + ] + expected_urlpatterns += [ + ('URLResolver', '^fake1/', 'django_deploy.tests.fake_app1.urls'), + ('URLResolver', '^fake2/', 'django_deploy.tests.fake_app2.urls'), + ] + + real_urlpatterns = root_urlconf.urlpatterns + self.assertEqual(len(expected_urlpatterns), len(real_urlpatterns)) + + for i, expected in enumerate(expected_urlpatterns): + real = real_urlpatterns[i] + real_class_name = real.__class__.__name__ + self.assertTrue(real_class_name.endswith(expected[0])) + # Django 2 vs. Django 1 + if real_class_name == 'URLPattern': # pragma: no cover + self.assertEqual(expected[1], str(real.pattern)) + self.assertTrue(real.callback.startswith("