Mercurial > piecrust2
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 +