From 0c81f3ecb41293df95de4d4de2b7b17610df96a8 Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 7 Feb 2014 00:00:00 +0100 Subject: [PATCH] initial import --- LICENSE | 21 + README.rst | 47 +++ blogtopoid/__init__.py | 38 ++ blogtopoid/blogtopoid.py | 414 +++++++++++++++++++ blogtopoid/commands.py | 133 ++++++ blogtopoid/decorators.py | 18 + blogtopoid/example/19700101 example.md | 19 + blogtopoid/example/19700101 pythonlogo.png | Bin 0 -> 8950 bytes blogtopoid/example/blogtopoid.config.example | 13 + blogtopoid/example/index.html | 27 ++ blogtopoid/example/page.html | 27 ++ blogtopoid/example/post.html | 28 ++ blogtopoid/test/__init__.py | 17 + blogtopoid/test/test_blogtopoid.py | 19 + blogtopoid/test/test_decorators.py | 29 ++ doc/Makefile | 177 ++++++++ doc/make.bat | 242 +++++++++++ doc/source/api.rst | 22 + doc/source/conf.py | 239 +++++++++++ doc/source/index.rst | 43 ++ doc/source/manual.rst | 72 ++++ doc/source/templates.rst | 74 ++++ post-receive.example | 12 + requirements.txt | 8 + setup.cfg | 7 + setup.py | 44 ++ tox.ini | 12 + 27 files changed, 1802 insertions(+) create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 blogtopoid/__init__.py create mode 100644 blogtopoid/blogtopoid.py create mode 100644 blogtopoid/commands.py create mode 100644 blogtopoid/decorators.py create mode 100644 blogtopoid/example/19700101 example.md create mode 100644 blogtopoid/example/19700101 pythonlogo.png create mode 100644 blogtopoid/example/blogtopoid.config.example create mode 100644 blogtopoid/example/index.html create mode 100644 blogtopoid/example/page.html create mode 100644 blogtopoid/example/post.html create mode 100644 blogtopoid/test/__init__.py create mode 100644 blogtopoid/test/test_blogtopoid.py create mode 100644 blogtopoid/test/test_decorators.py create mode 100644 doc/Makefile create mode 100644 doc/make.bat create mode 100644 doc/source/api.rst create mode 100644 doc/source/conf.py create mode 100644 doc/source/index.rst create mode 100644 doc/source/manual.rst create mode 100644 doc/source/templates.rst create mode 100755 post-receive.example create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tox.ini diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b08ab9c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-2014 Christoph Gebhardt. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..27a65b0 --- /dev/null +++ b/README.rst @@ -0,0 +1,47 @@ +blogtopoid +========== + +This is pre-alpha. Most things don't work yet. + +blogtopoid is a blog generator - it takes a bunch of posts, renders them +to HTML and saves them to a web directory. + +Structure +========= + +Work flow: + +- take all documents from pagesdir, render them and save them to outputdir/. +- take all posts from inputdir, render and save to outputdir/YYYY/mm/dd/. + post filename must be "YYYYMMDD post title.ext". +- generate index.html linking all posts. +- generate rss feed including all posts. +- generate listing pages for all used tags. +- pack and copy style files from style/ to outputdir/style/. + +Post formats +============ + +Currently posts and pages can either be markdown2 or reStructuredText. + +In posts and pages ``{{blogurl}}`` gets replaced with blogurl from +config, in style files ``{{styleurl}}`` with blogurl/style. + +Usage +===== + +- pip install blogtopoid +- run blogtopoid --quickstart +- run blogtopoid +- see post-receive.example for automatically deploying from git + commits. + +TODO +==== + +- don't pregenerate listings (index, tag-pages), move to template. +- TEST! +- make a shipable default template +- implement --post +- check hashes +- paginate index (not relevant with current index) diff --git a/blogtopoid/__init__.py b/blogtopoid/__init__.py new file mode 100644 index 0000000..6cb8a80 --- /dev/null +++ b/blogtopoid/__init__.py @@ -0,0 +1,38 @@ +""" +blogtopoid +""" +from __future__ import unicode_literals +from __future__ import print_function + +import pkg_resources +from argparse import ArgumentParser + + +#hashstore = Hashstore('hashstore.json') + +def main(): + """ run blogtopoid. + + '--quickstart': set up a new (empty) blog + '--post': generate new blog post + given no arguments blogtopoid renders all posts and pages. + """ + from . import commands + dist = pkg_resources.get_distribution("blogtopoid") + + parser = ArgumentParser(description="simple blog generator") + parser.add_argument('--version', action='version', version='%(prog)s ' + + dist.version) + parser.add_argument('--quickstart', help="create empty blog skeleton", + action="store_true") + parser.add_argument('--post', help="create new post", action="store_true") + args = parser.parse_args() + + if args.quickstart: + commands.quickstart() + + elif args.post: + commands.post() + + else: + commands.generate() diff --git a/blogtopoid/blogtopoid.py b/blogtopoid/blogtopoid.py new file mode 100644 index 0000000..d372aa2 --- /dev/null +++ b/blogtopoid/blogtopoid.py @@ -0,0 +1,414 @@ +""" +objects and functions of blogtopoid. +""" +from __future__ import unicode_literals +from __future__ import absolute_import + +import re +import os +import sys +import json +import glob +import codecs +import shutil +import hashlib +import datetime +try: + import ConfigParser +except ImportError: + import configparser as ConfigParser + +import yaml +import PyRSS2Gen +import markdown2 +from cssmin import cssmin +from slugify import slugify +from docutils.core import publish_parts +from jinja2 import Environment, FileSystemLoader +from pygments.formatters.html import HtmlFormatter + +from .decorators import singleton + + +tags = {} + + +@singleton +class Config(object): + """ parse blogtopoid.config """ + def __init__(self): + confparser = ConfigParser.SafeConfigParser() + confparser.read([ + '/etc/blogtopoid.config', + os.path.expanduser('~/.blogtopoid.config'), + 'blogtopoid.config', + ]) + + if not confparser.has_section('general'): + print('please set up either /etc/blogtopid.config, {}, ' + 'or blogtopoid.config in current dir.'.format( + os.path.expanduser('~/.blogtopoid.config') + )) + sys.exit(1) + + self.blogtitle = confparser.get('general', 'blogtitle') + self.blogdescription = confparser.get('general', 'blogdescription') + self.outputdir = confparser.get('general', 'outputdir') + self.inputdir = confparser.get('general', 'inputdir') + self.pagesdir = confparser.get('general', 'pagesdir') + self.styledir = confparser.get('general', 'styledir') + self.templatedir = confparser.get('general', 'templatedir') + self.blogurl = confparser.get('general', 'blogurl') + self.supported_blogtypes = \ + confparser.get('general', 'supported_blogtypes').split(',') + self.mdextras = confparser.get('md', 'mdextras').split(',') + + +class Tag(object): + """ a tag, with colour and posts """ + def __init__(self, name): + self.name = name + self.posts = [] + + def colour(self): + """ return html colour for Tag. """ + return hashlib.md5(self.name.encode('utf-8')).hexdigest()[:6] + + +class Post(object): + """ all info about a blog post. + + fields: + * body: rendered HTML body + * filename: source filename on disk + * extension: type of post, inferred from filename + * date: publish date, inferred from filename + * title: post title, inferred from filename + * hash: hash of post source file + * outputpath: relative part for post (e.g. 1970/01/01/) + * outfile: full path + filename the rendered post will have + * link: URI the post will have + """ + def __init__(self, filename): + config = Config() + + self.inputdir = config.inputdir + self.body = None + self.filename = filename + self.extension = os.path.splitext(filename)[1].lower() + self.date = datetime.date(int(filename[0:4]), int(filename[4:6]), + int(filename[6:8])) + self.title = os.path.splitext(filename[9:])[0] + # self.hash = hashstore.hashfile( + # os.path.join(self.inputdir, filename) + # ) + self.outputpath = os.path.join(filename[0:4], filename[4:6], + filename[6:8]) + self.outfile = os.path.join( + self.outputpath, "{}.html".format(slugify(self.title)) + ) + self.link = "{}{}".format(config.blogurl, self.outfile) + self.tags = [] + + def render(self, pages): + """ generate post html and write to disk """ + config = Config() + + print("writing {}".format(os.path.join(config.outputdir, + self.outfile))) + + # parse front matter + re_yaml = re.compile(r'(^---\s*$(?P.*?)^---\s*$)' + '?(?P.*)', re.M | re.S) + post_file = codecs.open( + os.path.join(self.inputdir, self.filename), 'r', 'utf8', + ).read() + file_info = re_yaml.match(post_file) + yamlstring = file_info.groupdict().get('yaml') + post_yaml = yaml.load(yamlstring) if yamlstring else {} + post_content = file_info.groupdict().get('content') + + if type(self) == Post and 'tags' in post_yaml: + for tag_text in post_yaml.get('tags').split(','): + if tag_text not in tags: + tags[tag_text] = Tag(tag_text) + self.tags.append(tags[tag_text]) + tags[tag_text].posts.append(self) + + # make output directory for post + outputdir = config.outputdir + if not os.path.exists(os.path.join(outputdir, self.outputpath)): + os.makedirs(os.path.join(outputdir, self.outputpath)) + + # copy supplementary files to ouput dir + for infile in os.listdir(unicode(self.inputdir)): + if ((os.path.splitext(infile)[1].lower() not in + config.supported_blogtypes) and + infile[0:8] == self.date.strftime('%Y%m%d')): + shutil.copy( + os.path.join(self.inputdir, infile), + os.path.join(outputdir, self.outputpath, infile[9:]) + ) + + # load jinja2 template + env = Environment(loader=FileSystemLoader(config.templatedir)) + post_template = env.get_template('post.html') + + # actually render post + add_style = '' + if self.extension == '.md': + self.body = markdown2.markdown( + post_content, + extras=config.mdextras, + ) + elif self.extension == '.rst': + rst = publish_parts( + post_content, + writer_name='html' + ) + add_style = rst['stylesheet'] + self.body = rst['html_body'] + else: + return + + # write post to disk + codecs.open( + os.path.join(config.outputdir, self.outfile), + 'w', + 'utf-8' + ).write( + post_template.render( + config=config, + post=self, + pages=pages, + add_style=add_style, + ) + ) + + +class Page(Post): + """ all info about a blog page. + + fields: + * body: rendered HTML body + * filename: source filename on disk + * extension: type of post, inferred from filename + * title: post title, inferred from filename + * hash: hash of post source file + * outfile: full path + filename the rendered post will have + * link: URI the post will have + """ + def __init__(self, filename): + config = Config() + + self.inputdir = config.pagesdir + self.body = None + self.filename = filename + self.extension = os.path.splitext(filename)[1].lower() + self.title = os.path.splitext(filename)[0] + # self.hash = hashstore.hashfile( + # os.path.join(self.inputdir, filename) + # ) + self.outputpath = '' + self.outfile = "{}.html".format(slugify(self.title)) + self.link = "{}{}".format(config.blogurl, self.outfile) + + +class Hashstore(object): + """ store file hashes in a json file """ + def __init__(self, jsonfile): + self.filename = jsonfile + try: + with open(self.filename, 'r') as filehandler: + self.store = json.load(filehandler) + except (IOError, ValueError): + self.store = {} + + def get(self, objname): + """ look up saved hash for objname + + :param objname: key to look for + :return: hash or None + """ + if objname in self.store: + return self.store[objname] + else: + return None + + def set(self, objname, objhash): + """ save calculated hash for objname + + :param objname: key to save + :param objhash: hash to save + """ + self.store[objname] = objhash + with open(self.filename, 'w') as filehandler: + json.dump(self.store, filehandler) + + @staticmethod + def hashfile(filename, blocksize=65536): + """ calculate sha256 hash of a files contents. + + :param filename: file to calculate hash of + :param blocksize: read file in chunks of blocksize + :return: sha256 hexdigest + """ + with open(filename, 'r') as afile: + hasher = hashlib.sha256() + buf = afile.read(blocksize) + while len(buf) > 0: + hasher.update(buf) + buf = afile.read(blocksize) + return hasher.hexdigest() + + +def generate_feed(posts): + """ write feed.rss to disk + + :param posts: post objs to generate feed for. + :type posts: list of Post + """ + config = Config() + + rssitems = [ + PyRSS2Gen.RSSItem( + title=post.title, + description=post.body, + link=post.link, + guid=PyRSS2Gen.Guid(post.link), + pubDate=datetime.datetime.combine(post.date, + datetime.datetime.min.time()), + ) for post in posts + ] + PyRSS2Gen.RSS2( + title=config.blogtitle, + description=config.blogdescription, + link=config.blogurl, + lastBuildDate=datetime.datetime.now(), + items=rssitems + ).write_xml( + open(os.path.join(config.outputdir, 'feed.rss'), 'w') + ) + + +def generate_index(posts, pages): + """ write index.html to disk + + :param posts: post objs to generate index for. + :type posts: list of Post + """ + config = Config() + + # load jinja2 template + env = Environment(loader=FileSystemLoader(config.templatedir)) + post_template = env.get_template('index.html') + + # generate index from index.md + # TODO move markdown to md module + indexpage = "# {}\n".format(config.blogtitle) + for post in posts: + indexpage += "* {}: ".format( + post.date.strftime('%Y-%m-%d') + ) + for tag in post.tags: # TODO HTML?! + indexpage += ' '.format( + tag.colour(), + tag.name, + tag.name, + ) + indexpage += "[{}]({})\n".format( + post.title, + post.outfile, + ) + ihtml = markdown2.markdown(indexpage, extras=config.mdextras) + ihtml = post_template.render( + config=config, + body=ihtml, + pages=pages, + ) + codecs.open( + os.path.join(config.outputdir, 'index.html'), + 'w', + 'utf-8', + ).write(ihtml) + + +def prepare_style(outputdir, blogurl): + """ read and process style/ directory """ + config = Config() + # copy static files + if not os.path.exists(os.path.join(outputdir, 'style')): + os.makedirs(os.path.join(outputdir, 'style')) + + # copy supplementary files to ouput dir + for filename in os.listdir(config.styledir): + if os.path.splitext(filename)[1].lower() != '.css': + shutil.copy( + os.path.join(config.styledir, filename), + os.path.join(outputdir, 'style') + ) + + # write possible syntax highlights + codecs.open( + os.path.join(config.styledir, 'pygments.css'), + 'w', + 'utf-8', + ).write(HtmlFormatter().get_style_defs('.codehilite')) + # cat all css files together + allcss = "" + for cssfile in glob.iglob(os.path.join(config.styledir, '*.css')): + allcss = allcss + codecs.open(cssfile, 'r', 'utf-8').read() + allcss = allcss.replace('{{styleurl}}', "{}style/".format(blogurl)) + + # minimise and write css + codecs.open( + os.path.join(outputdir, 'style', 'style.css'), + 'w', + 'utf-8' + ).write(cssmin(allcss, wrap=1000)) + + +def generate_tag_indeces(tagobjs, pages): + """ write tags to disk + + :param pages: pages objs for sidebar + :type pages: list of Page + :param tagobjs: tag objs to generate indeces for. + :type tagobjs: dict (str, Tag) + """ + config = Config() + + # make output directory for tags + outputdir = config.outputdir + if not os.path.exists(os.path.join(outputdir, 'tags')): + os.makedirs(os.path.join(outputdir, 'tags')) + + # load jinja2 template + env = Environment(loader=FileSystemLoader(config.templatedir)) + post_template = env.get_template('index.html') + + # generate index from index.md + # TODO move markdown to md module + for tag in tagobjs.values(): + tagpage = "# {}\n".format(tag.name) + for post in tag.posts: + tagpage += "* {}: ".format( + post.date.strftime('%Y-%m-%d') + ) + tagpage += "[{}]({}{})\n".format( + post.title, + config.blogurl, + post.outfile, + ) + ihtml = markdown2.markdown(tagpage, extras=config.mdextras) + ihtml = post_template.render( + config=config, + body=ihtml, + pages=pages, + ) + codecs.open( + os.path.join(config.outputdir, 'tags', '{}.html'.format(tag.name)), + 'w', + 'utf-8', + ).write(ihtml) diff --git a/blogtopoid/commands.py b/blogtopoid/commands.py new file mode 100644 index 0000000..c51590c --- /dev/null +++ b/blogtopoid/commands.py @@ -0,0 +1,133 @@ +""" commands for blogtopoid script +""" +from __future__ import unicode_literals +from __future__ import absolute_import + +import os +import sys +import codecs +import ConfigParser +import pkg_resources + +from .blogtopoid import (Config, generate_index, generate_feed, + generate_tag_indeces, prepare_style, tags, Post, Page) + + +def quickstart(): + """ ask for all configuration options and write example files. + """ + # read config template + config_file = pkg_resources.resource_stream( + __name__, + 'example/blogtopoid.config.example', + ) + + # query user for config options + config = ConfigParser.SafeConfigParser() + config.readfp(config_file) + for section in config.sections(): + for option in config.options(section): + answer = raw_input( + "{} [default: {}] = ".format(option, + config.get(section, option)) + ) + if answer: + config.set(section, option, answer) + config.write(codecs.open('blogtopoid.config', 'w', 'utf8')) + + # make post and pages dirs + if not os.path.exists(config.get('general', 'inputdir')): + os.makedirs(config.get('general', 'inputdir')) + if not os.path.exists(config.get('general', 'pagesdir')): + os.makedirs(config.get('general', 'pagesdir')) + if not os.path.exists(config.get('general', 'styledir')): + os.makedirs(config.get('general', 'styledir')) + if not os.path.exists(config.get('general', 'templatedir')): + os.makedirs(config.get('general', 'templatedir')) + + # copy example post to inputdir/ + hw_post = pkg_resources.resource_stream( + __name__, + 'example/19700101 example.md', + ) + codecs.open( + os.path.join( + config.get('general', 'inputdir'), '19700101 example.md' + ), 'w', 'utf8' + ).write(hw_post.read()) + + hw_image = pkg_resources.resource_stream( + __name__, + 'example/19700101 pythonlogo.png', + ) + open(os.path.join( + config.get('general', 'inputdir'), '19700101 pythonlogo.png' + ), 'wb').write(hw_image.read()) + + # copy example template files + for template_filename in ['index.html', 'page.html', 'post.html']: + template_file = pkg_resources.resource_stream( + __name__, + 'example/{}'.format(template_filename), + ) + codecs.open(os.path.join( + config.get('general', 'templatedir'), template_filename + ), 'w', 'utf8').write(template_file.read()) + + # done setting up + sys.exit(0) + + +def post(): + """ ask for YAML front-matter options, create empty post + and start editor + """ + print('not yet implemented') + sys.exit(1) + + +def generate(): + """ generate HTML + """ + config = Config() + + pages = [] + for infile in os.listdir(unicode(config.pagesdir)): + if os.path.splitext(infile)[1].lower() in config.supported_blogtypes: + page = Page(infile) + pages.append(page) + for page in pages: + page.render(pages) + + posts = [] + for infile in os.listdir(unicode(config.inputdir)): + if os.path.splitext(infile)[1].lower() in config.supported_blogtypes: + post = Post(infile) + # if hashstore.get(infile) == post.hash: + # print("already processed") + # continue + + posts.append(post) + # hashstore.set(infile, post.hash) + + # sort posts by publish date + posts.sort(key=lambda p: p.date, reverse=True) + + # render post htmls + for post in posts: + post.render(pages) + + # generate index from index.md + generate_index(posts, pages) + + # generate rss feed + generate_feed(posts) + + # generate tag pages + generate_tag_indeces(tags, pages) + + # copy style dir to disk + prepare_style( + config.outputdir, + config.blogurl, + ) diff --git a/blogtopoid/decorators.py b/blogtopoid/decorators.py new file mode 100644 index 0000000..14c2947 --- /dev/null +++ b/blogtopoid/decorators.py @@ -0,0 +1,18 @@ +""" decorator functions. +""" +from __future__ import unicode_literals +from __future__ import absolute_import + + +def singleton(cls): + """ @singleton decorator, copied from pep-0318. + """ + instances = {} + + def getinstance(): + """ return existing instance of new + """ + if cls not in instances: + instances[cls] = cls() + return instances[cls] + return getinstance diff --git a/blogtopoid/example/19700101 example.md b/blogtopoid/example/19700101 example.md new file mode 100644 index 0000000..a137ba7 --- /dev/null +++ b/blogtopoid/example/19700101 example.md @@ -0,0 +1,19 @@ +--- +tags: example +--- +# Hello World! + +![Python Logo](pythonlogo.png) + +* Eins +* Zwei +* Drei + +```python +""" Hello World! """ +def hello(world): + return "Hello %s" % world + +if __name__=="__main__": + print hello('World') +``` diff --git a/blogtopoid/example/19700101 pythonlogo.png b/blogtopoid/example/19700101 pythonlogo.png new file mode 100644 index 0000000000000000000000000000000000000000..d42c54f9113718ec1747649767d4c6ae3a7dae67 GIT binary patch literal 8950 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 s78>#V? + + + + {{ title }} + + + {{ add_style }} + + + + +
+ {{ body|replace("{{blogurl}}", config.blogurl) }} +
+ + diff --git a/blogtopoid/example/page.html b/blogtopoid/example/page.html new file mode 100644 index 0000000..37e664b --- /dev/null +++ b/blogtopoid/example/page.html @@ -0,0 +1,27 @@ + + + + + {{ title }} + + + {{ add_style }} + + + + +
+ {{ post.body|replace("{{blogurl}}", config.blogurl) }} +
+ + diff --git a/blogtopoid/example/post.html b/blogtopoid/example/post.html new file mode 100644 index 0000000..d1cf943 --- /dev/null +++ b/blogtopoid/example/post.html @@ -0,0 +1,28 @@ + + + + + {{ title }} + + + {{ add_style }} + + + + +
+

{{ post.date }}

+ {{ post.body|replace("{{blogurl}}", config.blogurl) }} +
+ + diff --git a/blogtopoid/test/__init__.py b/blogtopoid/test/__init__.py new file mode 100644 index 0000000..7b5893d --- /dev/null +++ b/blogtopoid/test/__init__.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +from __future__ import absolute_import + +import unittest + +from . import test_blogtopoid +from blogtopoid.test import test_decorators + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTests(test_decorators) + test_suite.addTests(test_blogtopoid) + return test_suite + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) diff --git a/blogtopoid/test/test_blogtopoid.py b/blogtopoid/test/test_blogtopoid.py new file mode 100644 index 0000000..ff9f4b4 --- /dev/null +++ b/blogtopoid/test/test_blogtopoid.py @@ -0,0 +1,19 @@ +from __future__ import unicode_literals +from __future__ import absolute_import + +import hashlib +import unittest + +from blogtopoid import blogtopoid + + +class TestTag(unittest.TestCase): + def setUp(self): + self.tag1 = blogtopoid.Tag('tag') + self.tag2 = blogtopoid.Tag('tag2') + + def test_tag(self): + colour = hashlib.md5('tag'.encode('utf-8')).hexdigest()[:6] + self.assertEqual(self.tag1.colour(), colour) + + self.assertEqual(self.tag2.colour(), 'f32af7') diff --git a/blogtopoid/test/test_decorators.py b/blogtopoid/test/test_decorators.py new file mode 100644 index 0000000..569faed --- /dev/null +++ b/blogtopoid/test/test_decorators.py @@ -0,0 +1,29 @@ +import random +import unittest + +from blogtopoid.decorators import singleton + + +@singleton +class Singleton(object): + """ dummy singleton class for testing. """ + value = None + + +class TestSingleton(unittest.TestCase): + def setUp(self): + self.singleton = Singleton() + self.singleton.value = "1234" + + def test_singleton(self): + new_singleton = Singleton() + self.assertEqual(self.singleton, new_singleton) + self.assertEqual(new_singleton.value, '1234') + + value = random.random() + self.singleton.value = value + self.assertEqual(new_singleton.value, value) + + +if __name__ == '__main__': + unittest.main() diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..9aa32cb --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/blogtopoid.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/blogtopoid.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/blogtopoid" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/blogtopoid" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 0000000..0b15ce6 --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,242 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +set I18NSPHINXOPTS=%SPHINXOPTS% source +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\blogtopoid.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\blogtopoid.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/doc/source/api.rst b/doc/source/api.rst new file mode 100644 index 0000000..2640c29 --- /dev/null +++ b/doc/source/api.rst @@ -0,0 +1,22 @@ +Class API documentation +======================= + +blogtopoid +---------- +.. automodule:: blogtopoid + :members: + +blogtopoid.blogtopoid +--------------------- +.. automodule:: blogtopoid.blogtopoid + :members: + +blogtopoid.decorators +--------------------- +.. automodule:: blogtopoid.decorators + :members: + +blogtopoid.commands +------------------- +.. automodule:: blogtopoid.commands + :members: diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..88b5089 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,239 @@ +# -*- coding: utf-8 -*- +import os +import sys +sys.path.insert(0, os.path.abspath('../..')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'blogtopoid' +copyright = u'2014, Christoph Gebhardt' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.0.1a' +# The full version, including alpha/beta/rc tags. +release = '0.0.1a' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'blogtopoiddoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + 'papersize': 'a4paper', + 'pointsize': '10pt', + 'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'blogtopoid.tex', u'blogtopoid Documentation', + u'Christoph Gebhardt', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'blogtopoid', u'blogtopoid Documentation', + [u'Christoph Gebhardt'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'blogtopoid', u'blogtopoid Documentation', + u'Christoph Gebhardt', 'blogtopoid', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..bbaf7ae --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,43 @@ +########## +blogtopoid +########## + +Contents: + +.. toctree:: + :maxdepth: 2 + + manual + templates + api + +Quickstart +========== + +Install blogtopoid and dependencies:: + + python setup.py install + +chdir to an empty directory and run:: + + blogtopoid --quickstart + +This will query for configuration options, +and deploy an example post and basic templates. + +Edit the templates to your liking, write real +posts, and run:: + + blogtopoid + +``outputdir/`` now contains the generated HTML. +Put all files into a web accessible directory. + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/doc/source/manual.rst b/doc/source/manual.rst new file mode 100644 index 0000000..9af85ff --- /dev/null +++ b/doc/source/manual.rst @@ -0,0 +1,72 @@ +Manual Setup +============ + +Install dependencies +-------------------- + +All dependencies are listed in `requirements.txt`. Either install them by hand +or use pip:: + + pip install -r requirements.txt + +Install blogtopoid +------------------ + +You can either build a blogtopoid egg using setup.py to deploy, use install +blogtopoid directly:: + + python setup.py install + +Set up directory structure +-------------------------- + +Next you need to set up a few directories: + +outputdir + is where the generated blog is stored. This directory needs to either be + web accessible, or uploaded (rsync, ftp) to a web host. + +inputdir + is where you put your posts source files. filenames need to be + "YYYYMMDD post title.md" (or .rst) + +pagesdir + is where you put your static pages (e.g. about, contact, ...) + +styledir + contains all files needed to style your blog: CSS, fonts, graphic elements. + +templatedir + contains the blogs templates. see :doc:`templates` for details. + +Configure blogtopoid +-------------------- + +blogotpoid looks for a configuration file in the following places: + +* current directory, blogtopoid.config +* user home directory, .blogtopoid.config +* /etc/blogtopoid.config + +Create one of those according to `blogtopoid.config.example`. + +GIT hook +-------- + +If you want to automatically deploy your blog from git install a post-receive +hook that generates the blog in your repository. + +example (with blogotpoid + dependencies installed in a virtualenv):: + + #!/bin/sh + + # dir where the checked out project lives on the server + CHECKOUT_DIR=/home/chris/blog + # virtualenv directory to use + VENV_DIR=$CHECKOUT_DIR/venv + + # cd into checkout dir, git pull, generate blog + cd $CHECKOUT_DIR + GIT_DIR=$CHECKOUT_DIR/.git + GIT_WORK_TREE=$CHECKOUT_DIR /usr/bin/git pull -v + $VENV_DIR/bin/blogtopoid diff --git a/doc/source/templates.rst b/doc/source/templates.rst new file mode 100644 index 0000000..d5c59b7 --- /dev/null +++ b/doc/source/templates.rst @@ -0,0 +1,74 @@ +Templates +========= + +Structure +--------- + +A blogtopoid template consists of three Jinja2 files: + +* index.html + Used for generating the landing page and tag listings. + +* post.html + Used for generating a post view. + +* page.html + Used for generating a page view. + +Variables +--------- + +Each template gets passed a few variables by blogtopoid. + +**Common for all template files:** + +*config* + A config object. + + Useful properties are: + + *blogtitle* + The blogs title + *blogdescription* + The blogs short description + *blogurl* + The blogs (base) URL + +*pages* + A list of all blog pages. + + +**index.html only:** + +*body* + The preformatted page body. + +**post.html & page.html only:** + +*add_style* + Additional CSS directives, currently only generated by rST files. + +*post* + The current Post (resp. Page) object. + + Useful properties are: + + *date* + (Post only, not in Page) The posts date + *title* + The Posts/Pages title + *body* + The preformatted post/pages body. + +CSS & Co +-------- + +Drop CSS files and additional needed files (fonts, images) into your +configured styledir/. +All non-CSS contents will be copied to outputdir/style/. +CSS files will be concatenated, minimised, and copied to outputdir/style/style.css +and can be loaded from templates by including:: + + + +in the section. \ No newline at end of file diff --git a/post-receive.example b/post-receive.example new file mode 100755 index 0000000..e510116 --- /dev/null +++ b/post-receive.example @@ -0,0 +1,12 @@ +#!/bin/sh + +# dir where the checked out project lives on the server +CHECKOUT_DIR=/home/chris/blog +# virtualenv directory to use +VENV_DIR=$CHECKOUT_DIR/venv + +# cd into checkout dir, git pull, generate blog +cd $CHECKOUT_DIR +GIT_DIR=$CHECKOUT_DIR/.git +GIT_WORK_TREE=$CHECKOUT_DIR /usr/bin/git pull -v +$VENV_DIR/bin/blogtopoid diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f9d0acb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +Jinja2==2.7.2 +Pygments==1.6 +PyRSS2Gen==1.1 +PyYAML==3.10 +cssmin==0.2.0 +docutils==0.11 +markdown2==2.2.0 +python-slugify==0.0.7 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..11c7201 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[upload_sphinx] +upload-dir = doc/build/html diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6691a12 --- /dev/null +++ b/setup.py @@ -0,0 +1,44 @@ +""" package setup +""" +from os.path import join, dirname + +from pip.req import parse_requirements +from setuptools import setup, find_packages + + +def read_requirements(): + """ utility function to read in and parse requirements.txt """ + install_reqs = parse_requirements('requirements.txt') + return [str(ir.req) for ir in install_reqs] + + +setup( + name='blogtopoid', + version='0.0.1a', + author='chris', + author_email='cg@zknt.org', + packages=find_packages(), + include_package_data=True, + zip_safe=True, + url='https://github.com/hansenerd/blogtopoid/', + license='MIT', + description='simple blog generator', + long_description=open(join(dirname(__file__), 'README.rst')).read(), + classifiers=[ + 'Development Status :: 2 - Pre-Alpha', + 'Environment :: Console', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary', + + ], + install_requires=read_requirements(), + entry_points={ + 'console_scripts': ['blogtopoid = blogtopoid:main'], + }, + setup_requires=["pip", "setuptools_git >= 0.3"], + exclude_package_data={'': ['.gitignore']}, + test_suite="blogtopoid.test", +) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..11b8be0 --- /dev/null +++ b/tox.ini @@ -0,0 +1,12 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py27, py33, pypy + +[testenv] +commands = {envpython} setup.py test +deps = +