changeset 0:a212a3f2e3ee

Initial commit.
author Ludovic Chabant <ludovic@chabant.com>
date Sat, 21 Dec 2013 14:44:02 -0800
parents
children aaa8fb7c8918
files .hgignore bin/chef bin/chef.cmd chef.py piecrust/__init__.py piecrust/app.py piecrust/cache.py piecrust/commands/__init__.py piecrust/commands/base.py piecrust/configuration.py piecrust/decorators.py piecrust/environment.py piecrust/pathutil.py piecrust/plugins.py
diffstat 12 files changed, 736 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Sat Dec 21 14:44:02 2013 -0800
@@ -0,0 +1,4 @@
+syntax: glob
+*.pyc
+venv
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/chef	Sat Dec 21 14:44:02 2013 -0800
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+CHEF_DIR=`dirname $0`
+if `hash readlink 2>&-`; then
+    LINKED_EXE=`readlink $0`
+    if [ -n "$LINKED_EXE" ]; then
+        CHEF_DIR=`dirname $LINKED_EXE`
+    fi
+fi
+python $CHEF_DIR/../chef.py $@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/chef.cmd	Sat Dec 21 14:44:02 2013 -0800
@@ -0,0 +1,5 @@
+@echo off
+setlocal
+
+python %~dp0..\chef.py %*
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chef.py	Sat Dec 21 14:44:02 2013 -0800
@@ -0,0 +1,118 @@
+import sys
+import os.path
+import logging
+import argparse
+from piecrust.app import PieCrust, PieCrustConfiguration, APP_VERSION
+from piecrust.commands.base import CommandContext
+from piecrust.environment import StandardEnvironment
+from piecrust.pathutil import SiteNotFoundError, find_app_root
+
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO,
+        format="%(message)s")
+
+
+class NullPieCrust:
+    def __init__(self):
+        self.root = None
+        self.cache = False
+        self.debug = False
+        self.templates_dirs = []
+        self.pages_dir = []
+        self.posts_dir = []
+        self.plugins_dirs = []
+        self.theme_dir = None
+        self.cache_dir = None
+        self.config = PieCrustConfiguration()
+        self.env = StandardEnvironment(self)
+
+
+def main():
+    root = None
+    cache = True
+    debug = False
+    config_variant = None
+    i = 0
+    while i < len(sys.argv):
+        arg = sys.argv[i]
+        if arg.startswith('--root='):
+            root = os.path.expanduser(arg[len('--root='):])
+        elif arg == '--root':
+            root = sys.argv[i + 1]
+            ++i
+        elif arg.startswith('--config='):
+            config_variant = arg[len('--config='):]
+        elif arg == '--config':
+            config_variant = sys.argv[i + 1]
+            ++i
+        elif arg == '--no-cache':
+            cache = False
+        elif arg == '--debug':
+            debug = True
+
+        if arg[0] != '-':
+            break
+
+    if debug:
+        logger.setLevel(logging.DEBUG)
+
+    if root is None:
+        root = find_app_root()
+
+    if not root:
+        app = NullPieCrust()
+    else:
+        app = PieCrust(root, cache=cache)
+
+    # Handle a configuration variant.
+    if config_variant is not None:
+        if not root:
+            raise SiteNotFoundError()
+        app.config.applyVariant('variants/' + config_variant)
+
+    # Setup the arg parser.
+    parser = argparse.ArgumentParser(
+            description="The PieCrust chef manages your website.")
+    parser.add_argument('--version', action='version', version=('%(prog)s ' + APP_VERSION))
+    parser.add_argument('--root', help="The root directory of the website.")
+    parser.add_argument('--config', help="The configuration variant to use for this command.")
+    parser.add_argument('--debug', help="Show debug information.", action='store_true')
+    parser.add_argument('--no-cache', help="When applicable, disable caching.", action='store_true')
+    parser.add_argument('--quiet', help="Print only important information.", action='store_true')
+    parser.add_argument('--log', help="Send log messages to the specified file.")
+
+    commands = sorted(app.plugin_loader.getCommands(),
+            lambda a, b: cmp(a.name, b.name))
+    subparsers = parser.add_subparsers()
+    for c in commands:
+        def command_runner(r):
+            if root is None and c.requires_website:
+                raise SiteNotFoundError()
+            c.run(CommandContext(r, app))
+
+        p = subparsers.add_parser(c.name, help=c.description)
+        c.setupParser(p)
+        p.set_defaults(func=command_runner)
+
+    # Parse the command line.
+    result = parser.parse_args()
+
+    # Setup the logger.
+    if result.debug and result.quiet:
+        raise Exception("You can't specify both --debug and --quiet.")
+    if result.debug:
+        logger.setLevel(logging.DEBUG)
+    elif result.quiet:
+        logger.setLevel(logging.WARNING)
+    if result.log:
+        from logging.handlers import FileHandler
+        logger.addHandler(FileHandler(result.log))
+
+    # Run the command!
+    result.func(result)
+
+
+if __name__ == '__main__':
+    main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/app.py	Sat Dec 21 14:44:02 2013 -0800
@@ -0,0 +1,194 @@
+import json
+import os.path
+import types
+import codecs
+import hashlib
+import logging
+import yaml
+from cache import SimpleCache
+from decorators import lazy_property
+from plugins import PluginLoader
+from environment import StandardEnvironment
+from configuration import Configuration, merge_dicts
+
+
+APP_VERSION = '2.0.0alpha'
+CACHE_VERSION = '2.0'
+
+CACHE_DIR = '_cache'
+TEMPLATES_DIR = '_content/templates'
+PAGES_DIR = '_content/pages'
+POSTS_DIR = '_content/posts'
+PLUGINS_DIR = '_content/plugins'
+THEME_DIR = '_content/theme'
+
+CONFIG_PATH = '_content/config.yml'
+THEME_CONFIG_PATH = '_content/theme_config.yml'
+
+
+logger = logging.getLogger(__name__)
+
+
+class VariantNotFoundError(Exception):
+    def __init__(self, variant_path, message=None):
+        super(VariantNotFoundError, self).__init__(
+                message or ("No such configuration variant: %s" % variant_path))
+
+
+class PieCrustConfiguration(Configuration):
+    def __init__(self, paths=None, cache_dir=False):
+        super(PieCrustConfiguration, self).__init__()
+        self.paths = paths
+        self.cache_dir = cache_dir
+        self.fixups = []
+
+    def applyVariant(self, variant_path, raise_if_not_found=True):
+        variant = self.get(variant_path)
+        if variant is None:
+            if raise_if_not_found:
+                raise VariantNotFoundError(variant_path)
+            return
+        if not isinstance(variant, dict):
+            raise VariantNotFoundError(variant_path,
+                    "Configuration variant '%s' is not an array. "
+                    "Check your configuration file." % variant_path)
+        self.merge(variant)
+
+    def _load(self):
+        if self.paths is None:
+            self._values = self._validateAll({})
+            return
+        
+        path_times = filter(self.paths,
+                lambda p: os.path.getmtime(p))
+        cache_key = hashlib.md5("version=%s&cache=%s" % (
+                APP_VERSION, CACHE_VERSION))
+        
+        cache = None
+        if self.cache_dir:
+            cache = SimpleCache(self.cache_dir)
+
+        if cache is not None:
+            if cache.isValid('config.json', path_times):
+                config_text = cache.read('config.json')
+                self._values = json.loads(config_text)
+                
+                actual_cache_key = self._values.get('__cache_key')
+                if actual_cache_key == cache_key:
+                    return
+
+        values = {}
+        for i, p in enumerate(self.paths):
+            with codecs.open(p, 'r', 'utf-8') as fp:
+                loaded_values = yaml.load(fp.read())
+            for fixup in self.fixups:
+                fixup(i, loaded_values)
+            merge_dicts(values, loaded_values)
+
+        for fixup in self.fixups:
+            fixup(len(self.paths), values)
+
+        self._values = self._validateAll(values)
+
+        if cache is not None:
+            self._values['__cache_key'] = cache_key
+            config_text = json.dumps(self._values)
+            cache.write('config.json', config_text)
+
+
+class PieCrust(object):
+    def __init__(self, root, cache=True, debug=False, env=None):
+        self.root = root
+        self.debug = debug
+        self.cache = cache
+        self.plugin_loader = PluginLoader(self)
+        self.env = env
+        if self.env is None:
+            self.env = StandardEnvironment()
+        self.env.initialize(self)
+
+    @lazy_property
+    def config(self):
+        logger.debug("Loading site configuration...")
+        paths = []
+        if self.theme_dir:
+            paths.append(os.path.join(self.theme_dir, THEME_CONFIG_PATH))
+        paths.append(os.path.join(self.root, CONFIG_PATH))
+
+        config = PieCrustConfiguration(paths, self.cache_dir)
+        if self.theme_dir:
+            # We'll need to patch the templates directories to be relative
+            # to the site's root, and not the theme root.
+            def _fixupThemeTemplatesDir(index, config):
+                if index == 0:
+                    sitec = config.get('site')
+                    if sitec:
+                        tplc = sitec.get('templates_dirs')
+                        if tplc:
+                            if isinstance(tplc, types.StringTypes):
+                                tplc = [tplc]
+                            sitec['templates_dirs'] = filter(tplc,
+                                    lambda p: os.path.join(self.theme_dir, p))
+
+            config.fixups.append(_fixupThemeTemplatesDir)
+
+        return config
+
+    @lazy_property
+    def templates_dirs(self):
+        templates_dirs = self._get_configurable_dirs(TEMPLATES_DIR,
+                'site/templates_dirs')
+
+        # Also, add the theme directory, if nay.
+        if self.theme_dir:
+            default_theme_dir = os.path.join(self.theme_dir, TEMPLATES_DIR)
+            if os.path.isdir(default_theme_dir):
+                templates_dirs.append(default_theme_dir)
+
+        return templates_dirs
+
+    @lazy_property
+    def pages_dir(self):
+        return self._get_dir(PAGES_DIR)
+
+    @lazy_property
+    def posts_dir(self):
+        return self._get_dir(POSTS_DIR)
+
+    @lazy_property
+    def plugins_dirs(self):
+        return self._get_configurable_dirs(PLUGINS_DIR,
+                'site/plugins_dirs')
+
+    @lazy_property
+    def theme_dir(self):
+        return self._get_dir(THEME_DIR)
+
+    @lazy_property
+    def cache_dir(self):
+        if self.cache:
+            return os.path.join(self.root, CACHE_DIR)
+        return False
+
+    def _get_dir(self, default_rel_dir):
+        abs_dir = os.path.join(self.root, default_rel_dir)
+        if os.path.isdir(abs_dir):
+            return abs_dir
+        return False
+
+    def _get_configurable_dirs(self, default_rel_dir, conf_name):
+        dirs = []
+
+        # Add custom directories from the configuration.
+        conf_dirs = self.config.get(conf_name)
+        if conf_dirs is not None:
+            dirs += filter(conf_dirs,
+                    lambda p: os.path.join(self.root, p))
+
+        # Add the default directory if it exists.
+        default_dir = os.path.join(self.root, default_rel_dir)
+        if os.path.isdir(default_dir):
+            dirs.append(default_dir)
+
+        return dirs
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/cache.py	Sat Dec 21 14:44:02 2013 -0800
@@ -0,0 +1,46 @@
+import os
+import os.path
+import codecs
+
+
+class SimpleCache(object):
+    def __init__(self, base_dir):
+        if not os.path.isdir(base_dir):
+            os.makedirs(base_dir, 0755)
+        self.base_dir = base_dir
+
+    def isValid(self, path, time):
+        cache_time = self.getCacheTime(path)
+        if cache_time is None:
+            return False
+        if isinstance(time, list):
+            for t in time:
+                if cache_time < t:
+                    return False
+            return True
+        return cache_time >= time
+
+    def getCacheTime(self, path):
+        cache_path = self.getCachePath(path)
+        try:
+            return os.path.getmtime(cache_path)
+        except os.error:
+            return None
+
+    def has(self, path):
+        cache_path = self.getCachePath(path)
+        return os.path.isfile(cache_path)
+
+    def read(self, path):
+        cache_path = self.getCachePath(path)
+        with codecs.open(cache_path, 'r', 'utf-8') as fp:
+            return fp.read()
+
+    def write(self, path, content):
+        cache_path = self.getCachePath(path)
+        cache_dir = os.path.dirname(cache_path)
+        if not os.path.isdir(cache_dir):
+            os.makedirs(cache_dir, 0755)
+        with codecs.open(cache_path, 'w', 'utf-8') as fp:
+            fp.write(content)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/commands/base.py	Sat Dec 21 14:44:02 2013 -0800
@@ -0,0 +1,37 @@
+import logging
+
+
+logger = logging.getLogger(__name__)
+
+
+class CommandContext(object):
+    def __init__(self, args, app):
+        self.args = args
+        self.app = app
+
+
+class ChefCommand(object):
+    def __init__(self):
+        self.name = '__unknown__'
+        self.description = '__unknown__'
+        self.requires_website = True
+
+    def setupParser(self, parser):
+        raise NotImplementedError()
+
+    def run(self, ctx):
+        raise NotImplementedError()
+
+
+class RootCommand(ChefCommand):
+    def __init__(self):
+        super(RootCommand, self).__init__()
+        self.name = 'root'
+        self.description = "Gets the root directory of the current website."
+
+    def setupParser(self, parser):
+        pass
+
+    def run(self, ctx):
+        logger.info(ctx.app.root)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/configuration.py	Sat Dec 21 14:44:02 2013 -0800
@@ -0,0 +1,106 @@
+import re
+import yaml
+
+
+class Configuration(object):
+    def __init__(self, values=None, validate=True):
+        self._values = {}
+        if values is not None:
+            self.set_all(values, validate)
+
+    def set_all(self, values, validate=True):
+        if validate:
+            self._validateAll(values)
+        self._values = values
+
+    def get(self, key_path=None):
+        self._ensureLoaded()
+        if key_path is None:
+            return self._values
+        bits = key_path.split('/')
+        cur = self._values
+        for b in bits:
+            cur = cur.get(b)
+            if cur is None:
+                return None
+        return cur
+
+    def set(self, key_path, value):
+        self._ensureLoaded()
+        value = self._validateValue(key_path, value)
+        bits = key_path.split('/')
+        bitslen = len(bits)
+        cur = self._values
+        for i, b in enumerate(bits):
+            if i == bitslen - 1:
+                cur[b] = value
+            else:
+                if b not in cur:
+                    cur[b] = {}
+                cur = cur[b]
+
+    def has(self, key_path):
+        self._ensureLoaded()
+        bits = key_path.split('/')
+        cur = self._values
+        for b in bits:
+            cur = cur.get(b)
+            if cur is None:
+                return False
+        return True
+
+    def merge(self, other):
+        self._ensureLoaded()
+        merge_dicts(self._values, other._values,
+                validator=self._validateValue)
+
+    def _ensureLoaded(self):
+        if self._values is None:
+            self._load()
+
+    def _load(self):
+        self._values = self._validateAll({})
+
+    def _validateAll(self, values):
+        return values
+
+    def _validateValue(self, key_path, value):
+        return value
+
+
+def merge_dicts(local_cur, incoming_cur, parent_path=None, validator=None):
+    if validator is None:
+        validator = lambda k, v: v
+
+    for k, v in incoming_cur.iteritems():
+        key_path = k
+        if parent_path is not None:
+            key_path = parent_path + '/' + k
+
+        local_v = local_cur.get(k)
+        if local_v is not None:
+            if isinstance(v, dict) and isinstance(local_v, dict):
+                local_cur[k] = merge_dicts(local_v, v)
+            elif isinstance(v, list) and isinstance(local_v, list):
+                local_cur[k] = v + local_v
+            else:
+                local_cur[k] = validator(key_path, v)
+        else:
+            local_cur[k] = validator(key_path, v)
+
+
+header_regex = re.compile(
+        r'(---\s*\n)(?P<header>(.*\n)*?)^(---\s*\n)', re.MULTILINE)
+
+
+def parse_config_header(text):
+    m = header_regex.match(text)
+    if m is not None:
+        header = unicode(m.group('header'))
+        config = yaml.safe_load(header)
+        offset = m.end()
+    else:
+        config = {}
+        offset = 0
+    return config, offset
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/decorators.py	Sat Dec 21 14:44:02 2013 -0800
@@ -0,0 +1,17 @@
+import functools
+
+
+def lazy(f):
+    @functools.wraps(f)
+    def lazy_wrapper(*args, **kwargs):
+        if f.__lazyresult__ is None:
+            f.__lazyresult__ = f(*args, **kwargs)
+        return f.__lazyresult__
+
+    f.__lazyresult__ = None
+    return lazy_wrapper
+
+
+def lazy_property(f):
+    return property(lazy(f))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/environment.py	Sat Dec 21 14:44:02 2013 -0800
@@ -0,0 +1,52 @@
+import logging
+from decorators import lazy_property
+
+
+logger = logging.getLogger(__name__)
+
+
+class PageRepository(object):
+    pass
+
+
+class ExecutionContext(object):
+    pass
+
+
+class Environment(object):
+    def __init__(self):
+        self.page_repository = PageRepository()
+        self._execution_ctx = None
+
+    def initialize(self, app):
+        pass
+
+    @lazy_property
+    def pages(self):
+        logger.debug("Loading pages...")
+        return self._loadPages()
+
+    @lazy_property
+    def posts(self):
+        logger.debug("Loading posts...")
+        return self._loadPosts()
+
+    @lazy_property
+    def file_system(self):
+        return None
+
+    def get_execution_context(self, auto_create=False):
+        if auto_create and self._execution_ctx is None:
+            self._execution_ctx = ExecutionContext()
+        return self._execution_ctx
+
+    def _loadPages(self):
+        raise NotImplementedError()
+
+    def _loadPosts(self):
+        raise NotImplementedError()
+
+
+class StandardEnvironment(Environment):
+    pass
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/pathutil.py	Sat Dec 21 14:44:02 2013 -0800
@@ -0,0 +1,23 @@
+import os
+import os.path
+
+
+class SiteNotFoundError(Exception):
+    def __init__(self, root=None):
+        if not root:
+            root = os.getcwd()
+        Exception.__init__(self,
+                "No PieCrust website in '%s' "
+                "('_content/config.yml' not found!)." % root)
+
+
+def find_app_root(cwd=None):
+    if cwd is None:
+        cwd = os.getcwd()
+
+    while not os.path.isfile(os.path.join(cwd, '_content', 'config.yml')):
+        cwd = os.path.dirname(cwd)
+        if not cwd:
+            return None
+    return cwd
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/plugins.py	Sat Dec 21 14:44:02 2013 -0800
@@ -0,0 +1,123 @@
+import os
+from piecrust.commands.base import (RootCommand)
+
+
+class PieCrustPlugin(object):
+    def getFormatters(self):
+        return []
+
+    def getTemplateEngines(self):
+        return []
+
+    def getDataProviders(self):
+        return []
+
+    def getFileSystems(self):
+        return []
+
+    def getProcessors(self):
+        return []
+
+    def getImporters(self):
+        return []
+
+    def getCommands(self):
+        return []
+
+    def getRepositories(self):
+        return []
+
+    def getBakerAssistants(self):
+        return []
+
+    def initialize(self, app):
+        pass
+
+
+class BuiltInPlugin(PieCrustPlugin):
+    def __init__(self):
+        super(BuiltInPlugin, self).__init__()
+        self.name = '__builtin__'
+
+    def getCommands(self):
+        return [
+                RootCommand()]
+
+
+class PluginLoader(object):
+    def __init__(self, app):
+        self.app = app
+        self._plugins = None
+        self._pluginsMeta = None
+        self._componentCache = {}
+
+    @property
+    def plugins(self):
+        self._ensureLoaded()
+        return self._plugins
+
+    def getFormatters(self):
+        return self._getPluginComponents('getFormatters', True,
+                order_key=lambda f: f.priority)
+
+    def getTemplateEngines(self):
+        return self._getPluginComponents('getTemplateEngines', True)
+
+    def getDataProviders(self):
+        return self._getPluginComponents('getDataProviders')
+
+    def getFileSystems(self):
+        return self._getPluginComponents('getFileSystems')
+
+    def getProcessors(self):
+        return self._getPluginComponents('getProcessors', True,
+                order_key=lambda p: p.priority)
+
+    def getImporters(self):
+        return self._getPluginComponents('getImporters')
+
+    def getCommands(self):
+        return self._getPluginComponents('getCommands')
+
+    def getRepositories(self):
+        return self._getPluginComponents('getRepositories', True)
+
+    def getBakerAssistants(self):
+        return self._getPluginComponents('getBakerAssistants')
+
+    def _ensureLoaded(self):
+        if self._plugins is not None:
+            return
+
+        self._plugins = [BuiltInPlugin()]
+        self._pluginsMeta = {self._plugins[0].name: False}
+
+        for d in self.app.plugins_dirs:
+            _, dirs, __ = next(os.walk(d))
+            for dd in dirs:
+                self._loadPlugin(os.path.join(d, dd))
+
+        for plugin in self._plugins:
+            plugin.initialize(self.app)
+
+    def _loadPlugin(self, plugin_dir):
+        pass
+
+    def _getPluginComponents(self, name, initialize=False, order_cmp=None, order_key=None):
+        if name in self._componentCache:
+            return self._componentCache[name]
+
+        all_components = []
+        for plugin in self.plugins:
+            plugin_components = getattr(plugin, name)()
+            all_components += plugin_components
+            if initialize:
+                for comp in plugin_components:
+                    comp.initialize(self.app)
+
+        if order_cmp is not None or order_key is not None:
+            all_components.sort(cmp=order_cmp, key=order_key)
+
+        self._componentCache[name] = all_components
+        return all_components
+