@@ 1,265 1,271 @@
-#!/usr/bin/env python
-import argparse
-import fnmatch
-import os
-from os import path
-import re
-import shutil
-from SimpleHTTPServer import SimpleHTTPRequestHandler
-import SocketServer
-import string
-import subprocess
-import sys
-
-from docutils.core import publish_parts
-
-# TODO: ie http-equiv tag
-# TODO: generator meta tag?
-default_template = string.Template('''<!DOCTYPE html>
-<html><head>
- $favicon
- $title
- $stylesheet
- $scripts
-</head><body>
- $content
-</body></html>
-''')
-
-def _normpath(p):
- return path.normcase(path.normpath(path.abspath(p)))
-
-class GitError(Exception):
-
- def __init__(self, error):
- if isinstance(error, subprocess.CalledProcessError):
- Exception.__init__(self, 'Failed: %s\n%s' % (error.cmd, error.output))
- else:
- Exception.__init__(self, error)
-
-# TODO: show fatal rst syntax errors, e.g. include file that doesn't exist
-class LiveSiteHandler(SimpleHTTPRequestHandler):
- '''
- Can extend server behavior by replacing this class with a subclass of itself that overrides the
- do_* methods as usual.
- '''
-
- def do_GET(self):
- # TODO: expire immediately
- # TODO: race condition when reloading during a compile
- # TODO: tests for this
- self.server.site.compile()
- prev_cwd = os.getcwd()
- try:
- os.chdir(self.server.site.target)
- SimpleHTTPRequestHandler.do_GET(self)
- finally:
- os.chdir(prev_cwd)
-
-class DeadSimpleSite(object):
-
- def __init__(self, source):
- self.source = path.abspath(source)
- self.target = path.join(self.source, '_site')
-
- def _clean(self):
- for root, dirs, files in os.walk(self.target, topdown=False):
- for filename in files:
- target = path.join(root, filename)
- relpath = path.relpath(target, self.target)
- if relpath[0] == '.' and filename != '.htaccess':
- break
- os.remove(target)
- if path.relpath(root, self.target)[0] != '.' and len(os.listdir(root)) == 0:
- os.rmdir(root)
-
- def _git(self, *args):
- if not path.exists(self.target):
- os.makedirs(self.target)
- with open(os.devnull, 'w') as devnull:
- if args[0] == 'push':
- # Git reads username and password from console, so don't suppress output
- # Also, don't want to throw an error since most likely the user just mistyped
- subprocess.call(('git',) + args, cwd=self.target)
- else:
- return subprocess.check_output(('git',) + args, stderr=subprocess.STDOUT, cwd=self.target)
-
- def _render(self, source, template):
- relpath = path.relpath(source, self.source)
- depth = len(re.split(r'[/\\]', relpath)) - 1
- html_root = depth * '../'
- style_tag = '<link rel="stylesheet" href="%sstyle/global.css">' % html_root \
- if path.exists(path.join(self.source, 'style/global.css')) else ''
- script_tags = '\n'.join(['<script src="%s%s"></script>' % (html_root, script)
- for script in self._scripts()])
- favicon_tag = '<link rel="shortcut icon" href="favicon.ico">' \
- if path.exists(path.join(self.source, 'favicon.ico')) else ''
- with open(source) as source_file:
- parts = publish_parts(
- source=source_file.read(),
- source_path=source,
- writer_name='html',
- # TODO: smart quotes on
- # TODO: going to need something like this to get sensible title behavior
- # also, I don't like the default docinfo, e.g. authors, copyright, they
- # are related because both depend on being the first thing in the source
- #settings_overrides={'doctitle_xform': False}
- )
- return template.substitute(
- content = parts['html_body'],
- favicon = favicon_tag,
- title = '<title>%s</title>' % parts['title'],
- root = html_root,
- stylesheet = style_tag,
- scripts = script_tags)
-
- def _scripts(self):
- scripts = []
- for root, dirs, files in self._walk_source():
- # TODO: this walks the output directory - that's bad
- for script in fnmatch.filter(files, '*.js'):
- if script[0] == '.':
- continue
- script_path = path.join(root, script)
- relpath = path.relpath(script_path, self.source)
- # In *.nix, paths may contain backslashes. Don't worry about psychos who do that.
- scripts.insert(0, relpath.replace('\\', '/')) # for Windows
- return scripts
-
- def _walk_source(self):
- for root, dirs, files in os.walk(self.source):
- dirs.sort()
- files.sort()
- remove = [d for d in dirs if d[0] == '.' or d == '_site']
- for dirname in remove:
- dirs.remove(dirname)
- yield root, dirs, [f for f in files if f[0] != '.' or f == '.htaccess']
-
- def compile(self):
- self._clean()
- for root, dirs, files in self._walk_source():
- rel = path.relpath(root, self.source)
- if not path.exists(path.join(self.target, rel)):
- os.makedirs(path.join(self.target, rel))
- rst = {path.splitext(f)[0] for f in files if path.splitext(f)[1] == '.rst'}
- for rst_name in rst:
- source = path.join(root, rst_name + '.rst')
- template_path = path.join(root, rst_name + '.html')
- target = path.join(self.target, rel, rst_name + '.html')
- ancestor = rel
- while not path.exists(template_path):
- template_path = path.join(self.source, ancestor, '__template__.html')
- if not ancestor:
- break
- ancestor = path.dirname(ancestor)
- if path.exists(template_path):
- with open(template_path) as template_in:
- template = string.Template(template_in.read())
- else:
- template = default_template
- with open(target, 'w') as html_out:
- html_out.write(self._render(source, template))
- for filename in files:
- source = path.join(root, filename)
- target = path.join(self.target, rel, filename)
- if path.splitext(filename)[1] in ['.rst', '.html']:
- if path.splitext(filename)[0] in rst:
- continue
- if filename == '__template__.html':
- continue
- shutil.copy2(source, target)
-
- def serve(self, port=8000):
- server = SocketServer.TCPServer(('', port), LiveSiteHandler)
- server.site = self
- server.serve_forever()
-
- def publish(self, origin=None):
- ''' This will do the following:
- #. check whether _site is a git repo
- #. if not, clone the repository from Github
- #. checkout gh-pages branch
- #. if that fails, create a gh-pages branch
- #. compile site
- #. git addremove
- #. git commit
- #. git push gh-pages gh-pages
- '''
- def clone():
- if not origin:
- raise GitError('Origin required to clone remote repository')
- try:
- self._git('clone', origin, '.')
- except subprocess.CalledProcessError as e:
- raise GitError(e)
- # TODO: handle "user pages" as well as project pages, where site is on master branch
- try:
- self._git('--version')
- except subprocess.CalledProcessError as e:
- raise GitError('No git command found. Is git installed and on the path?')
- self._clean()
- if not path.exists(self.target):
- os.makedirs(self.target)
- try:
- gitdir = self._git('rev-parse', '--git-dir')
- except subprocess.CalledProcessError:
- clone()
- else:
- if not path.isabs(gitdir):
- gitdir = path.join(self.target, gitdir)
- if path.dirname(_normpath(gitdir)) != _normpath(self.target):
- clone()
- try:
- self._git('reset', '--hard')
- try:
- self._git('checkout', 'gh-pages')
- except subprocess.CalledProcessError:
- self._git('branch', 'gh-pages')
- self._git('checkout', 'gh-pages')
- else:
- self._git('pull')
- self.compile()
- self._git('add', '-A')
- self._git('commit', '-m', 'Dead Simple Site auto publish')
- self._git('push', '-u', 'origin', 'gh-pages')
- except subprocess.CalledProcessError as e:
- raise GitError(e)
-
-def cli(args=None):
-
- def serve(parsed):
- DeadSimpleSite(parsed.directory).serve(parsed.port)
-
- def compile(parsed):
- DeadSimpleSite(parsed.directory).compile()
-
- def publish(parsed):
- try:
- DeadSimpleSite(parsed.directory).publish(parsed.origin)
- except GitError as e:
- sys.stderr.write(str(e) + '\n')
- sys.exit(1)
-
- parser = argparse.ArgumentParser(description='Dead Simple Site generator')
- parser.add_argument('-d', '--directory', help='folder containing site source files', default='.')
- subs = parser.add_subparsers(title='subcommands')
-
- parser_serve = subs.add_parser('serve', help='serve the site for development')
- parser_serve.add_argument('-p', '--port', type=int, default=8000)
- parser_serve.set_defaults(func=serve)
-
- parser_compile = subs.add_parser('compile', help='build the site')
- parser_compile.set_defaults(func=compile)
-
- parser_publish = subs.add_parser('publish', help='publish to Github pages')
- # TODO: arguments could be optional if the working dir is a repo on github, by
- # `git remote show origin`, or if _site is already a github repo
- parser_publish.add_argument('origin', nargs='?',
- help='Github URL to repository. Ignored if _site is already a repository.')
- parser_publish.set_defaults(func=publish)
-
- parsed = parser.parse_args(args)
- parsed.func(parsed)
-
-if __name__ == '__main__':
- cli()
+#!/usr/bin/env python
+import argparse
+import fnmatch
+import os
+from os import path
+import re
+import shutil
+from SimpleHTTPServer import SimpleHTTPRequestHandler
+import SocketServer
+import string
+import subprocess
+import sys
+
+from docutils.core import publish_parts
+
+# TODO: ie http-equiv tag
+# TODO: generator meta tag?
+# TODO: header with home link
+# TODO: footer with mod date info
+default_template = string.Template('''<!DOCTYPE html>
+<html><head>
+ $favicon
+ $title
+ $stylesheet
+ $scripts
+</head><body>
+ $content
+</body></html>
+''')
+
+def _normpath(p):
+ return path.normcase(path.normpath(path.abspath(p)))
+
+class GitError(Exception):
+
+ def __init__(self, error):
+ if isinstance(error, subprocess.CalledProcessError):
+ Exception.__init__(self, 'Failed: %s\n%s' % (error.cmd, error.output))
+ else:
+ Exception.__init__(self, error)
+
+# TODO: show fatal rst syntax errors, e.g. include file that doesn't exist
+class LiveSiteHandler(SimpleHTTPRequestHandler):
+ '''
+ Can extend server behavior by replacing this class with a subclass of itself that overrides the
+ do_* methods as usual.
+ '''
+
+ def do_GET(self):
+ # TODO: expire immediately
+ # TODO: race condition when reloading during a compile
+ # TODO: tests for this
+ self.server.site.compile()
+ prev_cwd = os.getcwd()
+ try:
+ os.chdir(self.server.site.target)
+ SimpleHTTPRequestHandler.do_GET(self)
+ finally:
+ os.chdir(prev_cwd)
+
+class DeadSimpleSite(object):
+
+ def __init__(self, source):
+ self.source = path.abspath(source)
+ self.target = path.join(self.source, '_site')
+
+ def _clean(self):
+ for root, dirs, files in os.walk(self.target, topdown=False):
+ for filename in files:
+ target = path.join(root, filename)
+ relpath = path.relpath(target, self.target)
+ if relpath[0] == '.' and filename != '.htaccess':
+ break
+ os.remove(target)
+ if path.relpath(root, self.target)[0] != '.' and len(os.listdir(root)) == 0:
+ os.rmdir(root)
+
+ def _git(self, *args):
+ if not path.exists(self.target):
+ os.makedirs(self.target)
+ with open(os.devnull, 'w') as devnull:
+ if args[0] == 'push':
+ # Git reads username and password from console, so don't suppress output
+ # Also, don't want to throw an error since most likely the user just mistyped
+ subprocess.call(('git',) + args, cwd=self.target)
+ else:
+ return subprocess.check_output(('git',) + args, stderr=subprocess.STDOUT, cwd=self.target)
+
+ def _render(self, source, template):
+ relpath = path.relpath(source, self.source)
+ depth = len(re.split(r'[/\\]', relpath)) - 1
+ html_root = depth * '../'
+ style_tag = '<link rel="stylesheet" href="%sstyle/global.css">' % html_root \
+ if path.exists(path.join(self.source, 'style/global.css')) else ''
+ script_tags = '\n'.join(['<script src="%s%s"></script>' % (html_root, script)
+ for script in self._scripts()])
+ favicon_tag = '<link rel="shortcut icon" href="favicon.ico">' \
+ if path.exists(path.join(self.source, 'favicon.ico')) else ''
+ with open(source) as source_file:
+ parts = publish_parts(
+ source=source_file.read(),
+ source_path=source,
+ writer_name='html',
+ # TODO: smart quotes on
+ # TODO: going to need something like this to get sensible title behavior
+ # also, I don't like the default docinfo, e.g. authors, copyright, they
+ # are related because both depend on being the first thing in the source
+ #settings_overrides={'doctitle_xform': False}
+ )
+ return template.substitute(
+ content = parts['html_body'],
+ favicon = favicon_tag,
+ title = '<title>%s</title>' % parts['title'],
+ root = html_root,
+ stylesheet = style_tag,
+ scripts = script_tags)
+
+ def _scripts(self):
+ scripts = []
+ for root, dirs, files in self._walk_source():
+ # TODO: this walks the output directory - that's bad
+ for script in fnmatch.filter(files, '*.js'):
+ if script[0] == '.':
+ continue
+ script_path = path.join(root, script)
+ relpath = path.relpath(script_path, self.source)
+ # In *.nix, paths may contain backslashes. Don't worry about psychos who do that.
+ scripts.insert(0, relpath.replace('\\', '/')) # for Windows
+ return scripts
+
+ def _walk_source(self):
+ for root, dirs, files in os.walk(self.source):
+ dirs.sort()
+ files.sort()
+ remove = [d for d in dirs if d[0] == '.' or d == '_site']
+ for dirname in remove:
+ dirs.remove(dirname)
+ yield root, dirs, [f for f in files if f[0] != '.' or f == '.htaccess']
+
+ def compile(self):
+ self._clean()
+ for root, dirs, files in self._walk_source():
+ rel = path.relpath(root, self.source)
+ if not path.exists(path.join(self.target, rel)):
+ os.makedirs(path.join(self.target, rel))
+ rst = {path.splitext(f)[0] for f in files if path.splitext(f)[1] == '.rst'}
+ for rst_name in rst:
+ source = path.join(root, rst_name + '.rst')
+ template_path = path.join(root, rst_name + '.html')
+ target = path.join(self.target, rel, rst_name + '.html')
+ ancestor = rel
+ while not path.exists(template_path):
+ template_path = path.join(self.source, ancestor, '__template__.html')
+ if not ancestor:
+ break
+ ancestor = path.dirname(ancestor)
+ if path.exists(template_path):
+ with open(template_path) as template_in:
+ template = string.Template(template_in.read())
+ else:
+ template = default_template
+ with open(target, 'w') as html_out:
+ html_out.write(self._render(source, template))
+ for filename in files:
+ source = path.join(root, filename)
+ target = path.join(self.target, rel, filename)
+ if path.splitext(filename)[1] in ['.rst', '.html']:
+ if path.splitext(filename)[0] in rst:
+ continue
+ if filename == '__template__.html':
+ continue
+ shutil.copy2(source, target)
+
+ def serve(self, port=8000):
+ SocketServer.TCPServer.allow_reuse_address = True
+ server = SocketServer.TCPServer(('', port), LiveSiteHandler)
+ server.site = self
+ server.serve_forever()
+
+ def publish(self, origin=None):
+ ''' This will do the following:
+ #. check whether _site is a git repo
+ #. if not, clone the repository from Github
+ #. checkout gh-pages branch
+ #. if that fails, create a gh-pages branch
+ #. compile site
+ #. git addremove
+ #. git commit
+ #. git push gh-pages gh-pages
+ '''
+ def clone():
+ if not origin:
+ raise GitError('Origin required to clone remote repository')
+ try:
+ # TODO: if the github repo is not initialized, clone fails, so make a first commit
+ self._git('clone', origin, '.')
+ except subprocess.CalledProcessError as e:
+ raise GitError(e)
+ # TODO: handle "user pages" as well as project pages, where site is on master branch
+ try:
+ self._git('--version')
+ except subprocess.CalledProcessError as e:
+ # TODO: on nix, this raises OSError
+ raise GitError('No git command found. Is git installed and on the path?')
+ self._clean()
+ if not path.exists(self.target):
+ os.makedirs(self.target)
+ try:
+ gitdir = self._git('rev-parse', '--git-dir')
+ except subprocess.CalledProcessError:
+ clone()
+ else:
+ if not path.isabs(gitdir):
+ gitdir = path.join(self.target, gitdir)
+ if path.dirname(_normpath(gitdir)) != _normpath(self.target):
+ clone()
+ try:
+ self._git('reset', '--hard')
+ try:
+ self._git('checkout', 'gh-pages')
+ except subprocess.CalledProcessError:
+ self._git('branch', 'gh-pages')
+ self._git('checkout', 'gh-pages')
+ else:
+ self._git('pull')
+ self.compile()
+ self._git('add', '-A')
+ self._git('commit', '-m', 'Dead Simple Site auto publish')
+ # TODO: revert commit if push fails
+ self._git('push', '-u', 'origin', 'gh-pages')
+ except subprocess.CalledProcessError as e:
+ raise GitError(e)
+
+def cli(args=None):
+
+ def serve(parsed):
+ DeadSimpleSite(parsed.directory).serve(parsed.port)
+
+ def compile(parsed):
+ DeadSimpleSite(parsed.directory).compile()
+
+ def publish(parsed):
+ try:
+ DeadSimpleSite(parsed.directory).publish(parsed.origin)
+ except GitError as e:
+ sys.stderr.write(str(e) + '\n')
+ sys.exit(1)
+
+ parser = argparse.ArgumentParser(description='Dead Simple Site generator')
+ parser.add_argument('-d', '--directory', help='folder containing site source files', default='.')
+ subs = parser.add_subparsers(title='subcommands')
+
+ parser_serve = subs.add_parser('serve', help='serve the site for development')
+ parser_serve.add_argument('-p', '--port', type=int, default=8000)
+ parser_serve.set_defaults(func=serve)
+
+ parser_compile = subs.add_parser('compile', help='build the site')
+ parser_compile.set_defaults(func=compile)
+
+ parser_publish = subs.add_parser('publish', help='publish to Github pages')
+ # TODO: arguments could be optional if the working dir is a repo on github, by
+ # `git remote show origin`, or if _site is already a github repo
+ parser_publish.add_argument('origin', nargs='?',
+ help='Github URL to repository. Ignored if _site is already a repository.')
+ parser_publish.set_defaults(func=publish)
+
+ parsed = parser.parse_args(args)
+ parsed.func(parsed)
+
+if __name__ == '__main__':
+ cli()
@@ 1,291 1,292 @@
-import os
-from os import path
-import re
-import shutil
-import subprocess
-import unittest
-
-import dss
-from dss import DeadSimpleSite
-
-def page_contain_assertion(testcase, filepath, invert=False):
- with open(filepath) as f:
- filetext = f.read()
- def page_contains(pattern):
- testcase.assertTrue(invert != bool(re.search(pattern, filetext, re.DOTALL)))
- return page_contains
-
-def clean(directory):
- if path.exists(directory):
- shutil.rmtree(directory)
-
-class TestCompile(unittest.TestCase):
-
- def setUp(self):
- clean('test/_site')
-
- def test_canary(self):
- ''' Do nothing '''
-
- def test_copy(self):
- self.assertFalse(path.exists('test/_site'))
- DeadSimpleSite('test').compile()
- self.assertTrue(path.exists('test/_site'))
- self.assertTrue(path.exists('test/_site/favicon.ico'))
- self.assertTrue(path.exists('test/_site/subdir/plain.html'))
-
- def test_render_default(self):
- DeadSimpleSite('test').compile()
- self.assertFalse(path.exists('test/_site/index.rst'))
- page_contains = page_contain_assertion(self, 'test/_site/index.html')
- # TODO: inline markup in title?
- page_contains(r'<head>.*<title>Becomes Title</title>.*</head>')
- page_contains(r'<body>.*Becomes content.*</body>')
- page_contains(r'<link rel="stylesheet" href="style/global\.css">')
- # TODO: html escape js paths
- page_contains(r'<script src="js/first/foo\.js"></script>')
- page_contains(r'<script src="js/bar\.js"></script>')
- page_contains(r'<link rel="shortcut icon" href="favicon.ico">')
-
- def test_render_default_subdir(self):
- DeadSimpleSite('test').compile()
- page_contains = page_contain_assertion(self, 'test/_site/subdir/page.html')
- page_contains(r'<link rel="stylesheet" href="\.\./style/global\.css">')
- # TODO: test for script alphabetical ordering
- # Notice this tests script depth ordering:
- page_contains(r'<script src="\.\./js/first/foo\.js"></script>.*<script src="\.\./js/bar\.js"></script>.*')
- # TODO: rst includes
- DeadSimpleSite('test').compile()
- not_page_contains = page_contain_assertion(self, 'test/_site/subdir/page.html', invert=True)
- # Tests that recompiling does not pick up scripts from the output directory
- not_page_contains(r'<script src=.*_site')
-
- def test_render_custom(self):
- DeadSimpleSite('test').compile()
- page_contains = page_contain_assertion(self, 'test/_site/custom.html')
- page_contains(r'<link rel="stylesheet" href="style/global\.css">')
- page_contains(r'<script src="js/first/foo\.js"></script>')
- page_contains(r'<script src="js/bar\.js"></script>')
- page_contains(r'Title From Content')
- page_contains(r'ReStructured Text content')
- page_contains(r'<p>Some html content</p>')
-
- def test_cleanup(self):
- ''' Compiling removes files and directories that do not exist in the source '''
- os.makedirs('test/_site/empty')
- open('test/_site/empty/bar', 'w').close()
- DeadSimpleSite('test').compile()
- self.assertFalse(path.exists('test/_site/empty/bar'))
- self.assertFalse(path.exists('test/_site/empty'))
-
- def test_ignore_dotfiles(self):
- ''' Ignores any files that begin with "." except .htaccess '''
- # TODO: worry about hidden files and such on windows?
- os.makedirs('test/_site/.ignored-empty-target-folder')
- os.makedirs('test/_site/.ignored-full-target-folder')
- open('test/_site/.ignored-target-file', 'w').close()
- open('test/_site/.ignored-full-target-folder/non-dotfile', 'w').close()
- DeadSimpleSite('test').compile()
- self.assertTrue(path.exists('test/_site/.htaccess'))
- self.assertTrue(path.exists('test/_site/subdir/.htaccess'))
- self.assertFalse(path.exists('test/_site/.ignored'))
- self.assertFalse(path.exists('test/_site/.ignored-folder'))
- self.assertTrue(path.exists('test/_site/.ignored-empty-target-folder'))
- self.assertTrue(path.exists('test/_site/.ignored-target-file'))
- self.assertTrue(path.exists('test/_site/.ignored-full-target-folder/non-dotfile'))
- not_page_contains = page_contain_assertion(self, 'test/_site/index.html', invert=True)
- not_page_contains(r'<script src=.*anything\.js')
- not_page_contains(r'\.ignored\.js')
-
-class TestBarrenCompile(unittest.TestCase):
-
- def setUp(self):
- clean('test-barren/_site')
-
- def test_render_barren(self):
- DeadSimpleSite('test-barren').compile()
- not_contains = page_contain_assertion(self, 'test-barren/_site/index.html', True)
- not_contains('<link rel="stylesheet"')
- not_contains('<script')
- not_contains('favicon')
-
- def test_cleans_htaccess(self):
- os.makedirs('test-barren/_site')
- open('test-barren/_site/.htaccess', 'w').close()
- DeadSimpleSite('test-barren').compile()
- self.assertFalse(path.exists('test-barren/_site/.htaccess'))
-
-class TestPublish(unittest.TestCase):
-
- prep_commands = [
- ('--version',),
- ('rev-parse', '--git-dir')]
- update_commands = [
- ('checkout', 'gh-pages'),
- ('pull',),
- ('add', '-A'),
- ('commit', '-m', 'Dead Simple Site auto publish'),
- ('push', '-u', 'origin', 'gh-pages',)]
- clone_commands = prep_commands \
- + [('clone', 'https://example.com/user/repo.git', '.'), ('reset', '--hard')] \
- + update_commands
-
- remote = 'https://example.com/user/repo.git'
-
- def setUp(self):
- self._real_git = DeadSimpleSite._git
-
- self.fail_git = None
- self.gitdir = '.git'
- self.site = DeadSimpleSite('test')
- self.git_invocations = []
-
- clean('test/_site')
- self.site.compile() # make sure the output dir does not start clean, to test cleanup
-
- def git(site, *gitargs):
- self.git_invocations.append(gitargs)
- if (gitargs[0] == self.fail_git):
- self.fail_git = None
- raise subprocess.CalledProcessError('foo', 'bar', 'baz')
- if (gitargs == ('rev-parse', '--git-dir')):
- self.assertTrue(path.exists(self.site.target))
- return self.gitdir
- if (gitargs[0] == 'clone'):
- self.assertTrue(gitargs[1] != None)
- if (gitargs[0] == 'reset'):
- self.assertFalse(path.exists('test/_site/index.html'))
- if (gitargs[0] == 'add'):
- self.assertTrue(path.exists('test/_site/index.html'))
- DeadSimpleSite._git = git
-
- def tearDown(self):
- DeadSimpleSite._git = self._real_git
-
- def test_creates_target(self):
- clean('test/_site')
- self.site.publish()
-
- def test_no_git(self):
- self.fail_git = '--version'
- with self.assertRaisesRegexp(dss.GitError, 'No git command found'):
- self.site.publish()
-
- def test_arbitrary_git_error(self):
- self.fail_git = 'reset'
- with self.assertRaisesRegexp(dss.GitError, 'baz'):
- self.site.publish()
-
- def test_existing_repo(self):
- self.site.publish()
- self.assertEqual(self.prep_commands + [('reset', '--hard')] + self.update_commands,
- self.git_invocations)
-
- def test_clone_when_source_in_git(self):
- self.gitdir = self.site.source + '/.git'
- self.site.publish(self.remote)
- self.assertEqual(self.clone_commands, self.git_invocations)
-
- def test_clone_when_source_not_in_git(self):
- self.fail_git = 'rev-parse'
- self.site.publish(self.remote)
- self.assertEqual(self.clone_commands, self.git_invocations)
-
- def test_clone_error_no_origin(self):
- self.gitdir = self.site.source + '/.git'
- with self.assertRaisesRegexp(dss.GitError, 'Origin required to clone remote repository'):
- self.site.publish()
-
- def test_clone_error_wrapping(self):
- self.gitdir = self.site.source + '/.git'
- self.fail_git = 'clone'
- with self.assertRaisesRegexp(dss.GitError, 'baz'):
- self.site.publish(self.remote)
-
- def test_no_gh_pages_branch(self):
- self.fail_git = 'checkout'
- self.site.publish()
- self.assertEqual(self.prep_commands
- + [ ('reset', '--hard'),
- ('checkout', 'gh-pages'),
- ('branch', 'gh-pages')]
- + [c for c in self.update_commands if c != ('pull',)],
- self.git_invocations)
-
-class TestTemplate(unittest.TestCase):
-
- def setUp(self):
- clean('test-templates/_site')
-
- def test_template_nest(self):
- DeadSimpleSite('test-templates').compile()
- self.assertFalse(path.exists('test-templates/_site/__template__.html'))
- self.assertFalse(path.exists('test-templates/_site/subdir/subsubdir/__template__.html'))
- index_contains = page_contain_assertion(self, 'test-templates/_site/index.html')
- index_contains(r'<a href="index\.html">Home</a>')
- sub_contains = page_contain_assertion(self, 'test-templates/_site/subdir/root-template.html')
- sub_contains(r'<a href="\.\./index\.html">Home</a>')
- not_sub_sub_contains = page_contain_assertion(self,
- 'test-templates/_site/subdir/subsubdir/nested.html', invert=True)
- not_sub_sub_contains(r'index\.html')
-
-class TestCli(unittest.TestCase):
-
- def setUp(self):
- self.compiled = None
- self.served = None
- self.served_on = None
- self.origin = None
-
- def dummy_compile(site):
- self.compiled = site.source
-
- def dummy_serve(site, port):
- self.served = site.source
- self.served_on = port
-
- def dummy_publish(site, origin):
- self.origin = origin
-
- self._real_compile = DeadSimpleSite.compile
- self._real_serve = DeadSimpleSite.serve
- self._real_publish = DeadSimpleSite.publish
- DeadSimpleSite.serve = dummy_serve
- DeadSimpleSite.compile = dummy_compile
- DeadSimpleSite.publish = dummy_publish
-
- def tearDown(self):
- DeadSimpleSite.serve = self._real_serve
- DeadSimpleSite.compile = self._real_compile
- DeadSimpleSite.publish = self._real_publish
-
- def test_compile(self):
- dss.cli(['compile'])
- self.assertTrue(self.compiled == path.abspath('.'))
-
- def test_serve(self):
- dss.cli(['serve'])
- self.assertTrue(self.served == path.abspath('.'))
- self.assertTrue(self.served_on == 8000)
-
- def test_path_arg(self):
- # TODO: SimpleHTTPRequestHandler still thinks its root is pwd
- dss.cli(['--directory', 'test', 'serve'])
- self.assertTrue(self.served == path.abspath('test'))
- dss.cli(['-d', 'test', 'serve'])
- self.assertTrue(self.served == path.abspath('test'))
-
- def test_port_arg(self):
- dss.cli(['serve', '-p', '8001'])
- self.assertTrue(self.served_on == 8001)
-
- def test_publish(self):
- dss.cli(['publish'])
- self.assertTrue(self.origin == None)
- dss.cli(['publish', 'http://example.com/user/repo'])
- self.assertTrue(self.origin == 'http://example.com/user/repo')
-
- # TODO: tests for errors
-
-if __name__ == '__main__':
- unittest.main()
+#!/usr/bin/env python
+import os
+from os import path
+import re
+import shutil
+import subprocess
+import unittest
+
+import dss
+from dss import DeadSimpleSite
+
+def page_contain_assertion(testcase, filepath, invert=False):
+ with open(filepath) as f:
+ filetext = f.read()
+ def page_contains(pattern):
+ testcase.assertTrue(invert != bool(re.search(pattern, filetext, re.DOTALL)))
+ return page_contains
+
+def clean(directory):
+ if path.exists(directory):
+ shutil.rmtree(directory)
+
+class TestCompile(unittest.TestCase):
+
+ def setUp(self):
+ clean('test/_site')
+
+ def test_canary(self):
+ ''' Do nothing '''
+
+ def test_copy(self):
+ self.assertFalse(path.exists('test/_site'))
+ DeadSimpleSite('test').compile()
+ self.assertTrue(path.exists('test/_site'))
+ self.assertTrue(path.exists('test/_site/favicon.ico'))
+ self.assertTrue(path.exists('test/_site/subdir/plain.html'))
+
+ def test_render_default(self):
+ DeadSimpleSite('test').compile()
+ self.assertFalse(path.exists('test/_site/index.rst'))
+ page_contains = page_contain_assertion(self, 'test/_site/index.html')
+ # TODO: inline markup in title?
+ page_contains(r'<head>.*<title>Becomes Title</title>.*</head>')
+ page_contains(r'<body>.*Becomes content.*</body>')
+ page_contains(r'<link rel="stylesheet" href="style/global\.css">')
+ # TODO: html escape js paths
+ page_contains(r'<script src="js/first/foo\.js"></script>')
+ page_contains(r'<script src="js/bar\.js"></script>')
+ page_contains(r'<link rel="shortcut icon" href="favicon.ico">')
+
+ def test_render_default_subdir(self):
+ DeadSimpleSite('test').compile()
+ page_contains = page_contain_assertion(self, 'test/_site/subdir/page.html')
+ page_contains(r'<link rel="stylesheet" href="\.\./style/global\.css">')
+ # TODO: test for script alphabetical ordering
+ # Notice this tests script depth ordering:
+ page_contains(r'<script src="\.\./js/first/foo\.js"></script>.*<script src="\.\./js/bar\.js"></script>.*')
+ # TODO: rst includes
+ DeadSimpleSite('test').compile()
+ not_page_contains = page_contain_assertion(self, 'test/_site/subdir/page.html', invert=True)
+ # Tests that recompiling does not pick up scripts from the output directory
+ not_page_contains(r'<script src=.*_site')
+
+ def test_render_custom(self):
+ DeadSimpleSite('test').compile()
+ page_contains = page_contain_assertion(self, 'test/_site/custom.html')
+ page_contains(r'<link rel="stylesheet" href="style/global\.css">')
+ page_contains(r'<script src="js/first/foo\.js"></script>')
+ page_contains(r'<script src="js/bar\.js"></script>')
+ page_contains(r'Title From Content')
+ page_contains(r'ReStructured Text content')
+ page_contains(r'<p>Some html content</p>')
+
+ def test_cleanup(self):
+ ''' Compiling removes files and directories that do not exist in the source '''
+ os.makedirs('test/_site/empty')
+ open('test/_site/empty/bar', 'w').close()
+ DeadSimpleSite('test').compile()
+ self.assertFalse(path.exists('test/_site/empty/bar'))
+ self.assertFalse(path.exists('test/_site/empty'))
+
+ def test_ignore_dotfiles(self):
+ ''' Ignores any files that begin with "." except .htaccess '''
+ # TODO: worry about hidden files and such on windows?
+ os.makedirs('test/_site/.ignored-empty-target-folder')
+ os.makedirs('test/_site/.ignored-full-target-folder')
+ open('test/_site/.ignored-target-file', 'w').close()
+ open('test/_site/.ignored-full-target-folder/non-dotfile', 'w').close()
+ DeadSimpleSite('test').compile()
+ self.assertTrue(path.exists('test/_site/.htaccess'))
+ self.assertTrue(path.exists('test/_site/subdir/.htaccess'))
+ self.assertFalse(path.exists('test/_site/.ignored'))
+ self.assertFalse(path.exists('test/_site/.ignored-folder'))
+ self.assertTrue(path.exists('test/_site/.ignored-empty-target-folder'))
+ self.assertTrue(path.exists('test/_site/.ignored-target-file'))
+ self.assertTrue(path.exists('test/_site/.ignored-full-target-folder/non-dotfile'))
+ not_page_contains = page_contain_assertion(self, 'test/_site/index.html', invert=True)
+ not_page_contains(r'<script src=.*anything\.js')
+ not_page_contains(r'\.ignored\.js')
+
+class TestBarrenCompile(unittest.TestCase):
+
+ def setUp(self):
+ clean('test-barren/_site')
+
+ def test_render_barren(self):
+ DeadSimpleSite('test-barren').compile()
+ not_contains = page_contain_assertion(self, 'test-barren/_site/index.html', True)
+ not_contains('<link rel="stylesheet"')
+ not_contains('<script')
+ not_contains('favicon')
+
+ def test_cleans_htaccess(self):
+ os.makedirs('test-barren/_site')
+ open('test-barren/_site/.htaccess', 'w').close()
+ DeadSimpleSite('test-barren').compile()
+ self.assertFalse(path.exists('test-barren/_site/.htaccess'))
+
+class TestPublish(unittest.TestCase):
+
+ prep_commands = [
+ ('--version',),
+ ('rev-parse', '--git-dir')]
+ update_commands = [
+ ('checkout', 'gh-pages'),
+ ('pull',),
+ ('add', '-A'),
+ ('commit', '-m', 'Dead Simple Site auto publish'),
+ ('push', '-u', 'origin', 'gh-pages',)]
+ clone_commands = prep_commands \
+ + [('clone', 'https://example.com/user/repo.git', '.'), ('reset', '--hard')] \
+ + update_commands
+
+ remote = 'https://example.com/user/repo.git'
+
+ def setUp(self):
+ self._real_git = DeadSimpleSite._git
+
+ self.fail_git = None
+ self.gitdir = '.git'
+ self.site = DeadSimpleSite('test')
+ self.git_invocations = []
+
+ clean('test/_site')
+ self.site.compile() # make sure the output dir does not start clean, to test cleanup
+
+ def git(site, *gitargs):
+ self.git_invocations.append(gitargs)
+ if (gitargs[0] == self.fail_git):
+ self.fail_git = None
+ raise subprocess.CalledProcessError('foo', 'bar', 'baz')
+ if (gitargs == ('rev-parse', '--git-dir')):
+ self.assertTrue(path.exists(self.site.target))
+ return self.gitdir
+ if (gitargs[0] == 'clone'):
+ self.assertTrue(gitargs[1] != None)
+ if (gitargs[0] == 'reset'):
+ self.assertFalse(path.exists('test/_site/index.html'))
+ if (gitargs[0] == 'add'):
+ self.assertTrue(path.exists('test/_site/index.html'))
+ DeadSimpleSite._git = git
+
+ def tearDown(self):
+ DeadSimpleSite._git = self._real_git
+
+ def test_creates_target(self):
+ clean('test/_site')
+ self.site.publish()
+
+ def test_no_git(self):
+ self.fail_git = '--version'
+ with self.assertRaisesRegexp(dss.GitError, 'No git command found'):
+ self.site.publish()
+
+ def test_arbitrary_git_error(self):
+ self.fail_git = 'reset'
+ with self.assertRaisesRegexp(dss.GitError, 'baz'):
+ self.site.publish()
+
+ def test_existing_repo(self):
+ self.site.publish()
+ self.assertEqual(self.prep_commands + [('reset', '--hard')] + self.update_commands,
+ self.git_invocations)
+
+ def test_clone_when_source_in_git(self):
+ self.gitdir = self.site.source + '/.git'
+ self.site.publish(self.remote)
+ self.assertEqual(self.clone_commands, self.git_invocations)
+
+ def test_clone_when_source_not_in_git(self):
+ self.fail_git = 'rev-parse'
+ self.site.publish(self.remote)
+ self.assertEqual(self.clone_commands, self.git_invocations)
+
+ def test_clone_error_no_origin(self):
+ self.gitdir = self.site.source + '/.git'
+ with self.assertRaisesRegexp(dss.GitError, 'Origin required to clone remote repository'):
+ self.site.publish()
+
+ def test_clone_error_wrapping(self):
+ self.gitdir = self.site.source + '/.git'
+ self.fail_git = 'clone'
+ with self.assertRaisesRegexp(dss.GitError, 'baz'):
+ self.site.publish(self.remote)
+
+ def test_no_gh_pages_branch(self):
+ self.fail_git = 'checkout'
+ self.site.publish()
+ self.assertEqual(self.prep_commands
+ + [ ('reset', '--hard'),
+ ('checkout', 'gh-pages'),
+ ('branch', 'gh-pages')]
+ + [c for c in self.update_commands if c != ('pull',)],
+ self.git_invocations)
+
+class TestTemplate(unittest.TestCase):
+
+ def setUp(self):
+ clean('test-templates/_site')
+
+ def test_template_nest(self):
+ DeadSimpleSite('test-templates').compile()
+ self.assertFalse(path.exists('test-templates/_site/__template__.html'))
+ self.assertFalse(path.exists('test-templates/_site/subdir/subsubdir/__template__.html'))
+ index_contains = page_contain_assertion(self, 'test-templates/_site/index.html')
+ index_contains(r'<a href="index\.html">Home</a>')
+ sub_contains = page_contain_assertion(self, 'test-templates/_site/subdir/root-template.html')
+ sub_contains(r'<a href="\.\./index\.html">Home</a>')
+ not_sub_sub_contains = page_contain_assertion(self,
+ 'test-templates/_site/subdir/subsubdir/nested.html', invert=True)
+ not_sub_sub_contains(r'index\.html')
+
+class TestCli(unittest.TestCase):
+
+ def setUp(self):
+ self.compiled = None
+ self.served = None
+ self.served_on = None
+ self.origin = None
+
+ def dummy_compile(site):
+ self.compiled = site.source
+
+ def dummy_serve(site, port):
+ self.served = site.source
+ self.served_on = port
+
+ def dummy_publish(site, origin):
+ self.origin = origin
+
+ self._real_compile = DeadSimpleSite.compile
+ self._real_serve = DeadSimpleSite.serve
+ self._real_publish = DeadSimpleSite.publish
+ DeadSimpleSite.serve = dummy_serve
+ DeadSimpleSite.compile = dummy_compile
+ DeadSimpleSite.publish = dummy_publish
+
+ def tearDown(self):
+ DeadSimpleSite.serve = self._real_serve
+ DeadSimpleSite.compile = self._real_compile
+ DeadSimpleSite.publish = self._real_publish
+
+ def test_compile(self):
+ dss.cli(['compile'])
+ self.assertTrue(self.compiled == path.abspath('.'))
+
+ def test_serve(self):
+ dss.cli(['serve'])
+ self.assertTrue(self.served == path.abspath('.'))
+ self.assertTrue(self.served_on == 8000)
+
+ def test_path_arg(self):
+ # TODO: SimpleHTTPRequestHandler still thinks its root is pwd
+ dss.cli(['--directory', 'test', 'serve'])
+ self.assertTrue(self.served == path.abspath('test'))
+ dss.cli(['-d', 'test', 'serve'])
+ self.assertTrue(self.served == path.abspath('test'))
+
+ def test_port_arg(self):
+ dss.cli(['serve', '-p', '8001'])
+ self.assertTrue(self.served_on == 8001)
+
+ def test_publish(self):
+ dss.cli(['publish'])
+ self.assertTrue(self.origin == None)
+ dss.cli(['publish', 'http://example.com/user/repo'])
+ self.assertTrue(self.origin == 'http://example.com/user/repo')
+
+ # TODO: tests for errors
+
+if __name__ == '__main__':
+ unittest.main()