diff --git a/setup.py b/setup.py index d7c4b44..bf1a062 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,7 @@ class CreatePythonEnvironment(MyCommand): setup( name='django-deploy', - version='0.1.dev0', + version='0.2.dev0', description='Helper to deploy django apps.', url='https://dev.heinzelwerk.de/git/python/django-deploy', maintainer='Jens Kleineheismann', diff --git a/src/django_deploy/__init__.py b/src/django_deploy/__init__.py index bb17839..c6b8f0f 100644 --- a/src/django_deploy/__init__.py +++ b/src/django_deploy/__init__.py @@ -1,2 +1,2 @@ -from .config import get_installed_apps, get_urlpatterns +from .hooks import get_installed_apps, get_urlpatterns from .main import main diff --git a/src/django_deploy/api.py b/src/django_deploy/api.py new file mode 100644 index 0000000..b9ca25c --- /dev/null +++ b/src/django_deploy/api.py @@ -0,0 +1,167 @@ +import errno +import importlib +import json +import os +import sys + +from .base_types import OrderedDict +from .config import DJANGO_SETTINGS_DIR +from .exceptions import DjangoDeployError + + +class DjangoProjectHooksConfig(OrderedDict): + def load(self, path=None): + if path is None: + path = self._json_file + if os.path.exists(path): + with open(path, 'r') as f: + items = json.load(f) + for item in items: + app_name = item.pop('APP') + self[app_name] = item + else: + self._clear() + + def write(self, path=None): + if path is None: + path = self._json_file + with open(path, 'w') as f: + items = [] + for key in self: + item = self[key] + item['APP'] = key + items.append(item) + json.dump(items, 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(DjangoProjectHooksConfig, 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() + + +class DjangoProject(object): + _hooks_config_name = 'django_deploy_hooks' + + @staticmethod + 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 _get_hooks_config_from_app(self, module_path, hooks_config_name=None): + if hooks_config_name is None: + hooks_config_name = self._hooks_config_name + + try: + app_obj = importlib.import_module(module_path) + except ImportError as e: + raise DjangoDeployError(e, code=errno.ENOPKG) + + try: + hooks_config = getattr(app_obj, hooks_config_name) + except AttributeError: + try: + hooks_config = importlib.import_module('{}.{}'.format(module_path, hooks_config_name)) + except ImportError as e: + raise DjangoDeployError('In {}: {}'.format(module_path, e), code=errno.ENOENT) + + return hooks_config + + def create(self, install_hooks=True): + project_dir = self._project_dir + + if not os.path.exists(project_dir): + os.makedirs(project_dir) + + settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_DIR) + if os.path.exists(settings_dir): + raise DjangoDeployError('directory already exists: {}'.format(settings_dir), code=errno.EEXIST) + + 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) + if exitval != os.EX_OK: # pragma: no cover + raise DjangoDeployError(exitval=exitval) + + if install_hooks: + self.install_hooks() + + def install_hooks(self): + project_dir = self._project_dir + + if not os.path.exists(project_dir): + raise DjangoDeployError('No such project directory: {}'.format(project_dir), + code=errno.ENOENT) + + 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') + json_file = os.path.join(settings_dir, 'django_deploy.json') + + if not os.path.exists(settings_dir): + raise DjangoDeployError('Existing project directory' + ' does not contain django project: {}'.format(project_dir), + code=errno.EISDIR) + + if os.path.exists(json_file): + raise DjangoDeployError('file already exists: {}'.format(json_file), code=errno.EEXIST) + + config = DjangoProjectHooksConfig(project_dir=project_dir) + config.write() + + text = '\n' + text += '# django-deploy\n' + text += 'from django_deploy import get_installed_apps\n' + text += 'INSTALLED_APPS = get_installed_apps(__file__, INSTALLED_APPS)\n' + text += '\n' + self._append_to_pythonfile(settings_file, text) + + text = '\n' + text += '# django-deploy\n' + text += 'from django_deploy import get_urlpatterns\n' + text += 'urlpatterns = get_urlpatterns(__file__, urlpatterns)\n' + text += '\n' + self._append_to_pythonfile(urlconf_file, text) + + def install_app(self, module_path, hooks_config_name=None): + hooks_config = self._get_hooks_config_from_app(module_path, hooks_config_name) + installed_apps = getattr(hooks_config, 'INSTALLED_APPS', []) + + config = DjangoProjectHooksConfig(project_dir=self._project_dir) + if module_path not in config: + config[module_path] = {} + + config[module_path]['INSTALLED_APPS'] = installed_apps + config.write() + + def mount_app(self, module_path, route, hooks_config_name=None): + self.install_app(module_path, hooks_config_name=hooks_config_name) + hooks_config = self._get_hooks_config_from_app(module_path, hooks_config_name) + urlconf = getattr(hooks_config, 'ROOT_URLCONF', None) + + if urlconf is None: + raise DjangoDeployError('django deploy hooks from {} has no ROOT_URLCONF'.format(module_path), + code=errno.ENOLINK) + + config = DjangoProjectHooksConfig(project_dir=self._project_dir) + if module_path not in config: # pragma: no cover + config[module_path] = {} + + config[module_path]['MOUNT'] = [route, urlconf] + config.write() + + def __init__(self, project_dir): + self._project_dir = project_dir diff --git a/src/django_deploy/base_types.py b/src/django_deploy/base_types.py new file mode 100644 index 0000000..89297e1 --- /dev/null +++ b/src/django_deploy/base_types.py @@ -0,0 +1,29 @@ +class OrderedDict(object): + def _clear(self): + self._items = {} + self._order = [] + + def __init__(self): + self._items = {} + self._order = [] + + def __contains__(self, key): + return key in self._items + + def __getitem__(self, key): + return self._items[key] + + def __setitem__(self, key, value): + self._items[key] = value + if key not in self._order: + self._order.append(key) + + def __delitem__(self, key): + del self._items[key] + self._order.remove(key) + + def __iter__(self): + return iter(self._order) + + def __len__(self): + return len(self._items) diff --git a/src/django_deploy/config.py b/src/django_deploy/config.py index 53291ec..decb621 100644 --- a/src/django_deploy/config.py +++ b/src/django_deploy/config.py @@ -1,102 +1 @@ -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_deploy_hooks.py b/src/django_deploy/django_deploy_hooks.py new file mode 100644 index 0000000..20ad1be --- /dev/null +++ b/src/django_deploy/django_deploy_hooks.py @@ -0,0 +1,2 @@ +# INSTALLED_APPS = ['django_deploy'] +# ROOT_URLCONF = '.urls' diff --git a/src/django_deploy/django_settings.py b/src/django_deploy/django_settings.py deleted file mode 100644 index 3b21247..0000000 --- a/src/django_deploy/django_settings.py +++ /dev/null @@ -1,4 +0,0 @@ -# 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 index 229a4c1..20ed203 100644 --- a/src/django_deploy/exceptions.py +++ b/src/django_deploy/exceptions.py @@ -1,7 +1,17 @@ -class FatalError(Exception): - def __init__(self, *args, **kwargs): - self._exitval = kwargs.pop('exitval', None) - super(FatalError, self).__init__(*args, **kwargs) +class DjangoDeployError(Exception): + def __init__(self, message=None, code=None, exitval=None): + self._message = message + self._code = code + self._exitval = exitval + super(DjangoDeployError, self).__init__(message) + + @property + def message(self): + return self._message + + @property + def code(self): + return self._code @property def exitval(self): diff --git a/src/django_deploy/hooks.py b/src/django_deploy/hooks.py new file mode 100644 index 0000000..33f6a50 --- /dev/null +++ b/src/django_deploy/hooks.py @@ -0,0 +1,31 @@ +import os +from django.conf.urls import url, include + +from .api import DjangoProjectHooksConfig + + +def get_installed_apps(file_path, installed_apps): + settings_dir = os.path.dirname(os.path.abspath(file_path)) + config = DjangoProjectHooksConfig(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 = DjangoProjectHooksConfig(settings_dir=settings_dir) + urls_list = urlpatterns[:] + for app in config: + if 'MOUNT' in config[app]: + route, urlconf_module = config[app]['MOUNT'] + pattern = '^{}/'.format(route) + if urlconf_module.startswith('.'): + urlconf_module = '{}{}'.format(app, urlconf_module) + url_obj = url(pattern, include(urlconf_module)) + urls_list.append(url_obj) + return urls_list diff --git a/src/django_deploy/program.py b/src/django_deploy/program.py index 55043e8..ed3229f 100644 --- a/src/django_deploy/program.py +++ b/src/django_deploy/program.py @@ -1,10 +1,10 @@ import argparse -import importlib +import errno import os import sys -from .config import DJANGO_SETTINGS_DIR, DeployedAppsConfig -from .exceptions import FatalError +from .api import DjangoProject +from .exceptions import DjangoDeployError from .version import VERSION @@ -14,18 +14,29 @@ class Program(object): # pylint: disable=too-few-public-methods parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + VERSION) - parser.add_argument('-a', '--app', - action='append', dest='merge_apps', metavar='MODULE', - help='Merge settings from the django app MODULE\n' - 'Can be used multiple times') - parser.add_argument('-c', '--create', 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('--no-install-hooks', + action='store_true', dest='no_install_hooks', + help='Do not install django deploy hooks while creating django project directory') + + parser.add_argument('--install-hooks', + action='store_true', dest='install_hooks', + help='Install django deploy hooks into existing django project directory') + + parser.add_argument('-a', '--install-app', + action='append', dest='install_apps', metavar='MODULE', + help='Merge settings from django app MODULE into django project settings' + '. Can be used multiple times') + + parser.add_argument('-m', '--mount-app', + nargs=2, + action='append', dest='mount_apps', metavar=('MODULE', 'ROUTE'), + help='Merge settings from django app MODULE into django project settings' + ' and add the app root urlconf to django root urlconf as ROUTE' + '. Can be used multiple times') parser.add_argument('project_dir', metavar='PATH', help='The directory, where the django project is or will be installed.') @@ -41,111 +52,64 @@ class Program(object): # pylint: disable=too-few-public-methods return self._argparser.parse_args(argv) @staticmethod - 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') - - text = '\n' + comment + '\n' + code + '\n' - 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) + def _create_django_project(project_dir, install_hooks=True): + project = DjangoProject(project_dir) + project.create(install_hooks=install_hooks) @staticmethod - def _install_django_files(project_dir, overwrite=False): - if not os.path.exists(project_dir): - os.makedirs(project_dir) - - 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) - if exitval != os.EX_OK: # pragma: no cover - raise FatalError(exitval=exitval) + def _install_hooks(project_dir): + project = DjangoProject(project_dir) + project.install_hooks() @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) + def _install_app(project_dir, module_path): + project = DjangoProject(project_dir) + project.install_app(module_path) @staticmethod - def _add_installed_apps_from_app(project_dir, app): - settings_from_app = importlib.import_module('{}.django_settings'.format(app)) - wanted_apps = getattr(settings_from_app, 'ADD_INSTALLED_APPS', []) - - config = DeployedAppsConfig(project_dir=project_dir) - if app not in config: - config[app] = {} - - config[app]['INSTALLED_APPS'] = wanted_apps - config.write() - - @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', []) - - config = DeployedAppsConfig(project_dir=project_dir) - if app not in config: # pragma: no cover - config[app] = {} - - config[app]['urlpatterns'] = wanted_urls - config.write() - - 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 _mount_app(project_dir, module_path, route): + project = DjangoProject(project_dir) + project.mount_app(module_path, route) def __init__(self): self._argparser = argparse.ArgumentParser() self._setup_argparser(self._argparser) - def __call__(self, *args, **kwargs): + def __call__(self, *args, **kwargs): # pylint: disable=too-many-branches argv = kwargs.get('argv', None) cmd_args = self._parse_args(argv) exitval = os.EX_OK + exceptions = [] 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 + self._create_django_project(cmd_args.project_dir, install_hooks=not cmd_args.no_install_hooks) + if cmd_args.install_hooks: + self._install_hooks(cmd_args.project_dir) + if cmd_args.install_apps: + for app in cmd_args.install_apps: + try: + self._install_app(cmd_args.project_dir, app) + except DjangoDeployError as e: + if e.code in (errno.ENOPKG, errno.ENOENT): + exceptions.append(e) + continue + raise # pragma: no cover + if cmd_args.mount_apps: + for app, route in cmd_args.mount_apps: + try: + self._mount_app(cmd_args.project_dir, app, route) + except DjangoDeployError as e: + if e.code in (errno.ENOPKG, errno.ENOENT): + exceptions.append(e) + continue + raise # pragma: no cover + except DjangoDeployError as e: + exceptions.append(e) + + for e in exceptions: + if e.message: + sys.stderr.write('{}\n'.format(e.message)) + elif e.code: # pragma: no cover + sys.stderr.write('{}\n'.format(os.strerror(e.code))) + exitval = e.exitval or os.EX_SOFTWARE return exitval diff --git a/src/django_deploy/tests/base.py b/src/django_deploy/tests/base.py new file mode 100644 index 0000000..8d8e5f1 --- /dev/null +++ b/src/django_deploy/tests/base.py @@ -0,0 +1,137 @@ +import importlib +import os +import sys +import unittest +import mock +import pytest + +from ..config import DJANGO_SETTINGS_DIR + + +class DjangoDeployTestCase(unittest.TestCase): + @pytest.fixture(autouse=True) + def tmpdir(self, tmpdir): # pylint: disable=method-hidden + self.tmpdir = tmpdir + + @staticmethod + def _import_from_dir(module_name, directory): + sys.path.insert(0, directory) + module = importlib.import_module(module_name) + if sys.version_info.major == 2: # pragma: no cover + reload(module) # pylint: disable=undefined-variable + else: # pragma: no cover + importlib.reload(module) # pylint: disable=no-member + sys.path.pop(0) + return module + + def get_django_settings(self, project_dir): + module_name = 'settings' + directory = os.path.join(project_dir, DJANGO_SETTINGS_DIR) + return self._import_from_dir(module_name, directory) + + def get_django_root_urlconf(self, project_dir): + module_name = 'urls' + directory = os.path.join(project_dir, DJANGO_SETTINGS_DIR) + sys.modules['django.contrib'] = mock.MagicMock(name='django.contrib') + return self._import_from_dir(module_name, directory) + + 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_hooks(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 assert_no_django_deploy_hooks(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.assertFalse(os.path.isfile(django_deploy_json)) + + with open(settings_file, 'r') as f: + needle = 'from django_deploy import get_installed_apps' + haystack = f.read() + self.assertNotIn(needle, haystack) + + with open(urlconf_file, 'r') as f: + needle = 'from django_deploy import get_urlpatterns' + haystack = f.read() + self.assertNotIn(needle, haystack) + + def assert_installed_apps(self, project_dir, apps, default_apps=None): + settings = self.get_django_settings(project_dir) + + if default_apps is None: + expected_apps = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + ] + else: # pragma: no cover + expected_apps = default_apps + + expected_apps += apps + + self.assertListEqual(expected_apps, settings.INSTALLED_APPS) + + def assert_urlpatterns(self, project_dir, patterns): + root_urlconf = self.get_django_root_urlconf(project_dir) + + # 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 += patterns + + 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("