From 18613d3600f3f359e3f07d13ffdb91c7b2450ec2 Mon Sep 17 00:00:00 2001 From: Jens Kleineheismann Date: Fri, 15 Nov 2019 17:07:10 +0100 Subject: [PATCH 1/2] Make thinks more useful Extract functionality from program.Program to api, so it can be used from other python code. And instead of adding predefined urls to root_urlconf, we should /mount/ apps into a django project. --- src/django_deploy/api.py | 37 +++++++++++++++++++++++++ src/django_deploy/exceptions.py | 23 ++++++++++++--- src/django_deploy/program.py | 29 +++++++++---------- src/django_deploy/tests/test_api.py | 27 ++++++++++++++++++ src/django_deploy/tests/test_program.py | 4 +-- 5 files changed, 98 insertions(+), 22 deletions(-) create mode 100644 src/django_deploy/api.py create mode 100644 src/django_deploy/tests/test_api.py diff --git a/src/django_deploy/api.py b/src/django_deploy/api.py new file mode 100644 index 0000000..4723548 --- /dev/null +++ b/src/django_deploy/api.py @@ -0,0 +1,37 @@ +import errno +import os +import sys + +from .config import DJANGO_SETTINGS_DIR +from .exceptions import DjangoDeployError, FatalError + + +class DjangoProject(object): + def __init__(self, project_dir): + self._project_dir = project_dir + + def create(self): + 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 FatalError(exitval=exitval) + + def install_hooks(self): + pass + + def add_app(self, module): + pass + + def mount_app(self, module, route): + pass diff --git a/src/django_deploy/exceptions.py b/src/django_deploy/exceptions.py index 229a4c1..7304191 100644 --- a/src/django_deploy/exceptions.py +++ b/src/django_deploy/exceptions.py @@ -1,7 +1,22 @@ -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): + self._message = message + self._code = code + super(DjangoDeployError, self).__init__(message) + + @property + def message(self): + return self._message + + @property + def code(self): + return self._code + + +class FatalError(DjangoDeployError): + def __init__(self, message=None, code=None, exitval=None): + self._exitval = exitval + super(FatalError, self).__init__(message=message, code=code) @property def exitval(self): diff --git a/src/django_deploy/program.py b/src/django_deploy/program.py index 55043e8..b7dc5ea 100644 --- a/src/django_deploy/program.py +++ b/src/django_deploy/program.py @@ -3,8 +3,9 @@ import importlib import os import sys +from .api import DjangoProject from .config import DJANGO_SETTINGS_DIR, DeployedAppsConfig -from .exceptions import FatalError +from .exceptions import DjangoDeployError, FatalError from .version import VERSION @@ -63,21 +64,9 @@ class Program(object): # pylint: disable=too-few-public-methods return self._append_to_pythonfile(settings_file, text) @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_django_files(project_dir): + project = DjangoProject(project_dir) + project.create() @staticmethod def _install_django_deploy_files(project_dir, overwrite=False): @@ -147,5 +136,13 @@ class Program(object): # pylint: disable=too-few-public-methods for app in cmd_args.merge_apps: self._merge_app(cmd_args.project_dir, app) except FatalError as e: + if e.message: + sys.stderr.write('{}\n'.format(e.message)) exitval = e.exitval + except DjangoDeployError as e: + if e.message: + sys.stderr.write('{}\n'.format(e.message)) + elif e.code: + sys.stderr.write('{}\n'.format(os.strerror(e.code))) + exitval = os.EX_SOFTWARE return exitval diff --git a/src/django_deploy/tests/test_api.py b/src/django_deploy/tests/test_api.py new file mode 100644 index 0000000..08f19cc --- /dev/null +++ b/src/django_deploy/tests/test_api.py @@ -0,0 +1,27 @@ +import os +import unittest +import pytest + +from ..api import DjangoProject +from ..config import DJANGO_SETTINGS_DIR + + +class DjangoProjectTestCase(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 test_create(self): + project_dir = os.path.join(str(self.tmpdir), 'django') + project = DjangoProject(project_dir) + project.create() + self._assert_django_project(project_dir) diff --git a/src/django_deploy/tests/test_program.py b/src/django_deploy/tests/test_program.py index ae71d0f..f6a6e68 100644 --- a/src/django_deploy/tests/test_program.py +++ b/src/django_deploy/tests/test_program.py @@ -65,8 +65,8 @@ class ProgramTestCase(unittest.TestCase): 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') + self.assertEqual(os.EX_SOFTWARE, exitval, 'program() does not return os.EX_SOFTWARE' + ' when project directory is not empty') def test_enable_django_deploy(self): project_dir = os.path.join(str(self.tmpdir), 'pure_django') From f1f0c585b95354f71f4fa9225168d962a33295d6 Mon Sep 17 00:00:00 2001 From: Jens Kleineheismann Date: Sat, 16 Nov 2019 18:50:53 +0100 Subject: [PATCH 2/2] It is nice now. --- setup.py | 2 +- src/django_deploy/__init__.py | 2 +- src/django_deploy/api.py | 150 ++++++++++- src/django_deploy/base_types.py | 29 +++ src/django_deploy/config.py | 101 -------- src/django_deploy/django_deploy_hooks.py | 2 + src/django_deploy/django_settings.py | 4 - src/django_deploy/exceptions.py | 9 +- src/django_deploy/hooks.py | 31 +++ src/django_deploy/program.py | 153 +++++------ src/django_deploy/tests/base.py | 137 ++++++++++ src/django_deploy/tests/fake_app1/__init__.py | 1 + .../tests/fake_app1/django_deploy_hooks.py | 2 + .../tests/fake_app1/django_settings.py | 5 - .../tests/fake_app2/django_deploy_hooks.py | 2 + .../tests/fake_app2/django_settings.py | 5 - src/django_deploy/tests/test_api.py | 213 ++++++++++++++-- src/django_deploy/tests/test_base_types.py | 40 +++ .../tests/{test_config.py => test_hooks.py} | 28 +- src/django_deploy/tests/test_program.py | 240 ++++++++---------- src/django_deploy/version.py | 2 +- 21 files changed, 750 insertions(+), 408 deletions(-) create mode 100644 src/django_deploy/base_types.py create mode 100644 src/django_deploy/django_deploy_hooks.py delete mode 100644 src/django_deploy/django_settings.py create mode 100644 src/django_deploy/hooks.py create mode 100644 src/django_deploy/tests/base.py create mode 100644 src/django_deploy/tests/fake_app1/django_deploy_hooks.py delete mode 100644 src/django_deploy/tests/fake_app1/django_settings.py create mode 100644 src/django_deploy/tests/fake_app2/django_deploy_hooks.py delete mode 100644 src/django_deploy/tests/fake_app2/django_settings.py create mode 100644 src/django_deploy/tests/test_base_types.py rename src/django_deploy/tests/{test_config.py => test_hooks.py} (52%) 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 index 4723548..b9ca25c 100644 --- a/src/django_deploy/api.py +++ b/src/django_deploy/api.py @@ -1,16 +1,85 @@ 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, FatalError +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): - def __init__(self, project_dir): - self._project_dir = project_dir + _hooks_config_name = 'django_deploy_hooks' - def create(self): + @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): @@ -25,13 +94,74 @@ class DjangoProject(object): 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) + raise DjangoDeployError(exitval=exitval) + + if install_hooks: + self.install_hooks() def install_hooks(self): - pass + project_dir = self._project_dir - def add_app(self, module): - pass + if not os.path.exists(project_dir): + raise DjangoDeployError('No such project directory: {}'.format(project_dir), + code=errno.ENOENT) - def mount_app(self, module, route): - pass + 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 7304191..20ed203 100644 --- a/src/django_deploy/exceptions.py +++ b/src/django_deploy/exceptions.py @@ -1,7 +1,8 @@ class DjangoDeployError(Exception): - def __init__(self, message=None, code=None): + def __init__(self, message=None, code=None, exitval=None): self._message = message self._code = code + self._exitval = exitval super(DjangoDeployError, self).__init__(message) @property @@ -12,12 +13,6 @@ class DjangoDeployError(Exception): def code(self): return self._code - -class FatalError(DjangoDeployError): - def __init__(self, message=None, code=None, exitval=None): - self._exitval = exitval - super(FatalError, self).__init__(message=message, code=code) - @property def exitval(self): return self._exitval 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 b7dc5ea..ed3229f 100644 --- a/src/django_deploy/program.py +++ b/src/django_deploy/program.py @@ -1,11 +1,10 @@ import argparse -import importlib +import errno import os import sys from .api import DjangoProject -from .config import DJANGO_SETTINGS_DIR, DeployedAppsConfig -from .exceptions import DjangoDeployError, FatalError +from .exceptions import DjangoDeployError from .version import VERSION @@ -15,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.') @@ -42,107 +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) - - @staticmethod - def _install_django_files(project_dir): + def _create_django_project(project_dir, install_hooks=True): project = DjangoProject(project_dir) - project.create() + project.create(install_hooks=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_hooks(project_dir): + project = DjangoProject(project_dir) + project.install_hooks() @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() + def _install_app(project_dir, module_path): + project = DjangoProject(project_dir) + project.install_app(module_path) @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: - if e.message: - sys.stderr.write('{}\n'.format(e.message)) - 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: + elif e.code: # pragma: no cover sys.stderr.write('{}\n'.format(os.strerror(e.code))) - exitval = os.EX_SOFTWARE + 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("