From b883ba7f3b73ac884aebde1e962b3f316b479f62 Mon Sep 17 00:00:00 2001 From: David Joy Date: Tue, 6 Aug 2013 20:13:22 -0400 Subject: [PATCH] Move the quickstart generation into a function Quickstart files/dirs were generated with a short script at the end of quickstart.main(), but this makes things really difficult to use from a script because of the interactive dialog. Move the generation of files into quickstart.quickstart() so that a script can just pass in a config and get a quickstart directory. --- pelican/tests/test_tools.py | 206 +++++++++++++++++++ pelican/tools/pelican_quickstart.py | 182 +++++++++------- pelican/tools/templates/Makefile.in | 2 + pelican/tools/templates/develop_server.sh.in | 2 + 4 files changed, 315 insertions(+), 77 deletions(-) create mode 100644 pelican/tests/test_tools.py diff --git a/pelican/tests/test_tools.py b/pelican/tests/test_tools.py new file mode 100644 index 00000000..0aa28f29 --- /dev/null +++ b/pelican/tests/test_tools.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function + +# Standard lib +import os +import sys +import shutil +from io import StringIO +from tempfile import mkdtemp +import contextlib + +# Pelican +from pelican.tools import pelican_quickstart +from .support import unittest + +# python2 doesn't have mock in the standard lib +try: + mock = unittest.mock +except AttributeError: + mock = None + +if mock is None: + try: + import mock + except ImportError: + mock = None + + +# Helper classes +class FileSystemTest(unittest.TestCase): + """ Make a temporary dir to test file I/O """ + + def setUp(self): + prev_self = super(FileSystemTest, self) + if hasattr(prev_self, 'setUp'): + prev_self.setUp() + + self.tempdir = mkdtemp() + + def tearDown(self): + try: + if os.path.isdir(self.tempdir): + shutil.rmtree(self.tempdir) + except Exception: + pass + + prev_self = super(FileSystemTest, self) + if hasattr(prev_self, 'tearDown'): + prev_self.tearDown() + + def assertIsFile(self, path): + + msg = 'File not found: {}'.format(path) + self.assertTrue(os.path.isfile(path), msg=msg) + + def assertIsDir(self, path): + + msg = 'Directory not found: {}'.format(path) + self.assertTrue(os.path.isdir(path), msg=msg) + + def assertPathDoesntExist(self, path): + + msg = 'Path found unexpectedly: {}'.format(path) + self.assertFalse(os.path.exists(path), msg=msg) + + +@contextlib.contextmanager +def record_output(): + """ Record the standard output of a command + + Usage:: + + >>> with record_output() as out: + >>> print('hiiiiiii') + >>> out.getvalue() + 'hiiiiiii\n' + """ + + old_stdout = sys.stdout + try: + sys.stdout = StringIO() + yield sys.stdout + finally: + sys.stdout = old_stdout + + +# Tests +class TestMakeDirs(FileSystemTest): + + dirnames = ['content', 'output'] + + def test_make_dir_okay(self): + + for dirname in self.dirnames: + + dirpath = os.path.join(self.tempdir, dirname) + + self.assertPathDoesntExist(dirpath) + + pelican_quickstart.makedirs(dirpath) + + self.assertIsDir(dirpath) + + @unittest.skipIf(mock is None, "need to install mock") + def test_make_dir_error(self): + + dirpath = os.path.join(self.tempdir, 'evil') + + with mock.patch('os.makedirs') as makedirs: + makedirs.side_effect = OSError + with record_output() as out: + + pelican_quickstart.makedirs(dirpath) + + self.assertTrue(out.getvalue().startswith('Error:')) + + +class TestChmod(FileSystemTest): + + def test_make_dir_okay(self): + + filepath = os.path.join(self.tempdir, 'foo.txt') + + with open(filepath, 'wt') as fp: + fp.write('') + + pelican_quickstart.chmod(filepath, 493) # 0o755 + + self.assertTrue(os.access(filepath, os.R_OK)) + self.assertTrue(os.access(filepath, os.W_OK)) + self.assertTrue(os.access(filepath, os.X_OK)) + + @unittest.skipIf(mock is None, "need to install mock") + def test_make_dir_error(self): + + with mock.patch('os.chmod') as chmod: + chmod.side_effect = OSError + with record_output() as out: + pelican_quickstart.chmod('bad.txt', 493) + + self.assertTrue(out.getvalue().startswith('Error:')) + + +class TestMakeTemplate(FileSystemTest): + + template_files = [ + 'pelicanconf.py', 'publishconf.py', 'fabfile.py', + 'Makefile', 'develop_server.sh', + ] + + def test_make_template(self): + + for filename in self.template_files: + + filepath = os.path.join(self.tempdir, filename) + + self.assertPathDoesntExist(filepath) + + pelican_quickstart.make_template(filepath, {}) + + self.assertIsFile(filepath) + + @unittest.skipIf(mock is None, "need to install mock") + def test_make_template_fails(self): + + filepath = os.path.join(self.tempdir, 'pelicanconf.py') + + with mock.patch('codecs.open') as codecs_open: + codecs_open.side_effect = OSError + with record_output() as out: + + pelican_quickstart.make_template(filepath, {}) + + self.assertTrue(out.getvalue().startswith('Error:')) + + +class TestShellEscape(unittest.TestCase): + """ Shell escape things """ + + def test_escapes_boring_strings(self): + + conf = {'foo': 'something', + 'bar': '"something"'} + + res = pelican_quickstart.escape_shell(conf) + + self.assertEqual(res, {'foo': 'something', 'bar': '"something"'}) + self.assertIsNot(res, conf) + + def test_escapes_important_strings(self): + + conf = {'foo': 'something with spaces'} + + res = pelican_quickstart.escape_shell(conf) + + self.assertEqual(res, {'foo': '"something with spaces"'}) + self.assertIsNot(res, conf) + + def test_escapes_double_quotes(self): + + conf = {'foo': 'something with "quotes"'} + + res = pelican_quickstart.escape_shell(conf) + + self.assertEqual(res, {'foo': r'"something with \"quotes\""'}) + self.assertIsNot(res, conf) diff --git a/pelican/tools/pelican_quickstart.py b/pelican/tools/pelican_quickstart.py index 3f02355a..a526a7fa 100755 --- a/pelican/tools/pelican_quickstart.py +++ b/pelican/tools/pelican_quickstart.py @@ -72,6 +72,73 @@ def get_template(name, as_encoding='utf-8'): fd.close() +def makedirs(dirpath): + """ Makedirs ignoring errors + + :param dirpath: + The path to the directory to create + """ + + try: + os.makedirs(dirpath) + except OSError as e: + print('Error: {0}'.format(e)) + + +def chmod(path, mode): + """ Chmod, ignoring errors + + :param path: + Path to change the mode on + :param mode: + Mode to change + """ + + try: + os.chmod(path, mode) + except OSError as e: + print('Error: {0}'.format(e)) + + +def make_template(filepath, conf, as_encoding='utf-8'): + """ Make a template in the output dir + + :param filepath: + The path to the output file + :param conf: + The dictionary of config parameters + :param as_encoding: + The file encoding to write (default: 'utf-8') + """ + + try: + with codecs.open(filepath, 'w', as_encoding) as fd: + filename = os.path.basename(filepath) + for line in get_template(filename, as_encoding=as_encoding): + template = string.Template(line) + fd.write(template.safe_substitute(conf)) + fd.close() + except OSError as e: + print('Error: {0}'.format(e)) + + +def escape_shell(conf): + """ Shell escape the keys in the config + + :param conf: + The config dictionary + :returns: + A copy of the dictionary with strings escaped + """ + + new_conf = {} + for key, value in conf.items(): + if isinstance(value, six.string_types) and ' ' in value: + value = '"' + value.replace('"', '\\"') + '"' + new_conf[key] = value + return new_conf + + @decoding_strings def ask(question, answer=str_compat, default=None, l=None): if answer == str_compat: @@ -146,6 +213,43 @@ def ask(question, answer=str_compat, default=None, l=None): raise NotImplemented('Argument `answer` must be str_compat, bool, or integer') +def quickstart(conf, automation=True, develop=True): + """ Generate the templates + + :param conf: + The config to generate the templates with + """ + + if six.PY3: + python_binary = 'python3' + else: + python_binary = 'python' + + makedirs(os.path.join(conf['basedir'], 'content')) + makedirs(os.path.join(conf['basedir'], 'output')) + + conf_python = {key: repr(value) for key, value in conf.items()} + + make_template(os.path.join(conf['basedir'], 'pelicanconf.py'), conf_python) + make_template(os.path.join(conf['basedir'], 'publishconf.py'), conf) + + if automation: + + make_template(os.path.join(conf['basedir'], 'fabfile.py'), conf) + + makefile_conf = dict(conf) + makefile_conf['python_binary'] = python_binary + + make_template(os.path.join(conf['basedir'], 'Makefile'), makefile_conf) + + if develop: + shell_conf = escape_shell(conf) + shell_conf['python_binary'] = python_binary + + make_template(os.path.join(conf['basedir'], 'develop_server.sh'), shell_conf) + chmod(os.path.join(conf['basedir'], 'develop_server.sh'), 493) # mode 0o755 + + def main(): parser = argparse.ArgumentParser( description="A kickstarter for Pelican", @@ -211,83 +315,7 @@ needed by Pelican. if ask('Do you want to upload your website using S3?', answer=bool, default=False): CONF['s3_bucket'] = ask('What is the name of your S3 bucket?', str_compat, CONF['s3_bucket']) - try: - os.makedirs(os.path.join(CONF['basedir'], 'content')) - except OSError as e: - print('Error: {0}'.format(e)) - - try: - os.makedirs(os.path.join(CONF['basedir'], 'output')) - except OSError as e: - print('Error: {0}'.format(e)) - - try: - with codecs.open(os.path.join(CONF['basedir'], 'pelicanconf.py'), 'w', 'utf-8') as fd: - conf_python = dict() - for key, value in CONF.items(): - conf_python[key] = repr(value) - - for line in get_template('pelicanconf.py'): - template = string.Template(line) - fd.write(template.safe_substitute(conf_python)) - fd.close() - except OSError as e: - print('Error: {0}'.format(e)) - - try: - with codecs.open(os.path.join(CONF['basedir'], 'publishconf.py'), 'w', 'utf-8') as fd: - for line in get_template('publishconf.py'): - template = string.Template(line) - fd.write(template.safe_substitute(CONF)) - fd.close() - except OSError as e: - print('Error: {0}'.format(e)) - - if automation: - try: - with codecs.open(os.path.join(CONF['basedir'], 'fabfile.py'), 'w', 'utf-8') as fd: - for line in get_template('fabfile.py'): - template = string.Template(line) - fd.write(template.safe_substitute(CONF)) - fd.close() - except OSError as e: - print('Error: {0}'.format(e)) - try: - with codecs.open(os.path.join(CONF['basedir'], 'Makefile'), 'w', 'utf-8') as fd: - mkfile_template_name = 'Makefile' - py_v = 'PY=python' - if six.PY3: - py_v = 'PY=python3' - template = string.Template(py_v) - fd.write(template.safe_substitute(CONF)) - fd.write('\n') - for line in get_template(mkfile_template_name): - template = string.Template(line) - fd.write(template.safe_substitute(CONF)) - fd.close() - except OSError as e: - print('Error: {0}'.format(e)) - - if develop: - conf_shell = dict() - for key, value in CONF.items(): - if isinstance(value, six.string_types) and ' ' in value: - value = '"' + value.replace('"', '\\"') + '"' - conf_shell[key] = value - try: - with codecs.open(os.path.join(CONF['basedir'], 'develop_server.sh'), 'w', 'utf-8') as fd: - lines = list(get_template('develop_server.sh')) - py_v = 'PY=python\n' - if six.PY3: - py_v = 'PY=python3\n' - lines = lines[:4] + [py_v] + lines[4:] - for line in lines: - template = string.Template(line) - fd.write(template.safe_substitute(conf_shell)) - fd.close() - os.chmod((os.path.join(CONF['basedir'], 'develop_server.sh')), 493) # mode 0o755 - except OSError as e: - print('Error: {0}'.format(e)) + quickstart(CONF, automation=automation, develop=develop) print('Done. Your new project is available at %s' % CONF['basedir']) diff --git a/pelican/tools/templates/Makefile.in b/pelican/tools/templates/Makefile.in index f68694e3..ebbcbd20 100644 --- a/pelican/tools/templates/Makefile.in +++ b/pelican/tools/templates/Makefile.in @@ -1,3 +1,5 @@ +PY=$python_binary + PELICAN=$pelican PELICANOPTS=$pelicanopts diff --git a/pelican/tools/templates/develop_server.sh.in b/pelican/tools/templates/develop_server.sh.in index 16b61518..39d9797a 100755 --- a/pelican/tools/templates/develop_server.sh.in +++ b/pelican/tools/templates/develop_server.sh.in @@ -2,6 +2,8 @@ ## # This section should match your Makefile ## +PY=$python_binary + PELICAN=$pelican PELICANOPTS=$pelicanopts