changeset 666:81d9c3a3a0b5

internal: Get rid of the whole "sub cache" business. * Compute cache keys up front, so the cache directory is only chosen once. * Buffer up config variants to apply before loading the config. Makes it possible to cache variant-resulting configs, too. * Make a factory class to reuse the logic that creates the `PieCrust` object correctly for multi-process workers and such. * Add a test.
author Ludovic Chabant <ludovic@chabant.com>
date Thu, 03 Mar 2016 08:22:41 -0800
parents 5dc13c816045
children fc761964e1a7
files piecrust/app.py piecrust/appconfig.py piecrust/baking/baker.py piecrust/baking/worker.py piecrust/commands/builtin/baking.py piecrust/commands/builtin/serving.py piecrust/commands/builtin/util.py piecrust/configuration.py piecrust/environment.py piecrust/main.py piecrust/processing/pipeline.py piecrust/processing/sass.py piecrust/processing/worker.py piecrust/serving/middlewares.py piecrust/serving/procloop.py piecrust/serving/server.py piecrust/serving/util.py piecrust/serving/wrappers.py piecrust/wsgiutil/__init__.py tests/bakes/test_variant.yaml tests/conftest.py
diffstat 21 files changed, 297 insertions(+), 237 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/app.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/app.py	Thu Mar 03 08:22:41 2016 -0800
@@ -23,15 +23,15 @@
 
 class PieCrust(object):
     def __init__(self, root_dir, cache=True, debug=False, theme_site=False,
-                 env=None):
+                 env=None, cache_key=None):
         self.root_dir = root_dir
         self.debug = debug
         self.theme_site = theme_site
         self.plugin_loader = PluginLoader(self)
+        self.cache_key = cache_key or 'default'
 
         if cache:
-            cache_dir = os.path.join(self.cache_dir, 'default')
-            self.cache = ExtensibleCache(cache_dir)
+            self.cache = ExtensibleCache(self.cache_dir)
         else:
             self.cache = NullExtensibleCache()
 
@@ -82,7 +82,7 @@
                 if srcc is not None:
                     for sn, sc in srcc.items():
                         sc['realm'] = REALM_THEME
-            config.fixups.append(_fixupThemeSources)
+            config.addFixup(_fixupThemeSources)
 
         self.env.stepTimer('SiteConfigLoad', time.perf_counter() - start_time)
         return config
@@ -124,13 +124,7 @@
 
     @cached_property
     def cache_dir(self):
-        return os.path.join(self.root_dir, CACHE_DIR)
-
-    @property  # Not a cached property because its result can change.
-    def sub_cache_dir(self):
-        if self.cache.enabled:
-            return self.cache.base_dir
-        return None
+        return os.path.join(self.root_dir, CACHE_DIR, self.cache_key)
 
     @cached_property
     def sources(self):
@@ -197,18 +191,6 @@
                 return tax
         return None
 
-    def useSubCache(self, cache_name, cache_key):
-        cache_hash = hashlib.md5(cache_key.encode('utf8')).hexdigest()
-        cache_dir = os.path.join(self.cache_dir,
-                                 '%s_%s' % (cache_name, cache_hash))
-        self._useSubCacheDir(cache_dir)
-
-    def _useSubCacheDir(self, cache_dir):
-        assert cache_dir
-        logger.debug("Moving cache to: %s" % cache_dir)
-        self.cache = ExtensibleCache(cache_dir)
-        self.env._onSubCacheDirChanged(self)
-
     def _get_dir(self, default_rel_dir):
         abs_dir = os.path.join(self.root_dir, default_rel_dir)
         if os.path.isdir(abs_dir):
@@ -234,13 +216,42 @@
         return dirs
 
 
+
 def apply_variant_and_values(app, config_variant=None, config_values=None):
     if config_variant is not None:
-        logger.debug("Applying configuration variant '%s'." % config_variant)
-        app.config.applyVariant('variants/' + config_variant)
+        logger.debug("Adding configuration variant '%s'." % config_variant)
+        variant_path = os.path.join(
+                app.root_dir, 'configs', '%s.yml' % config_variant)
+        app.config.addVariant(variant_path)
 
     if config_values is not None:
         for name, value in config_values:
-            logger.debug("Setting configuration '%s' to: %s" % (name, value))
-            app.config.set(name, value)
+            logger.debug("Adding configuration override '%s': %s" % (name, value))
+            app.config.addVariantValue(name, value)
+
 
+class PieCrustFactory(object):
+    def __init__(
+            self, root_dir, *,
+            cache=True, cache_key=None,
+            config_variant=None, config_values=None,
+            debug=False, theme_site=False):
+        self.root_dir = root_dir
+        self.cache = cache
+        self.cache_key = cache_key
+        self.config_variant = config_variant
+        self.config_values = config_values
+        self.debug = debug
+        self.theme_site = theme_site
+
+    def create(self):
+        app = PieCrust(
+                self.root_dir,
+                cache=self.cache,
+                cache_key=self.cache_key,
+                debug=self.debug,
+                theme_site=self.theme_site)
+        apply_variant_and_values(
+                app, self.config_variant, self.config_values)
+        return app
+
--- a/piecrust/appconfig.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/appconfig.py	Thu Mar 03 08:22:41 2016 -0800
@@ -14,7 +14,7 @@
 from piecrust.cache import NullCache
 from piecrust.configuration import (
         Configuration, ConfigurationError, ConfigurationLoader,
-        merge_dicts, visit_dict)
+        get_dict_value, set_dict_value, merge_dicts, visit_dict)
 from piecrust.sources.base import REALM_USER, REALM_THEME
 
 
@@ -22,55 +22,100 @@
 
 
 class VariantNotFoundError(Exception):
-    def __init__(self, variant_path, message=None):
+    def __init__(self, variant_name, message=None):
         super(VariantNotFoundError, self).__init__(
                 message or ("No such configuration variant: %s" %
-                            variant_path))
+                            variant_name))
+
+
+def _make_variant_fixup(variant_name, raise_if_not_found):
+    def _variant_fixup(index, config):
+        if index != -1:
+            return
+        try:
+            try:
+                v = get_dict_value(config, 'variants/%s' % variant_name)
+            except KeyError:
+                raise VariantNotFoundError(variant_name)
+            if not isinstance(v, dict):
+                raise VariantNotFoundError(
+                        variant_name,
+                        "Configuration variant '%s' is not an array. "
+                        "Check your configuration file." % variant_name)
+            merge_dicts(config, v)
+        except VariantNotFoundError:
+            if raise_if_not_found:
+                raise
+
+    return _variant_fixup
 
 
 class PieCrustConfiguration(Configuration):
     def __init__(self, paths=None, cache=None, values=None, validate=True,
                  theme_config=False):
         super(PieCrustConfiguration, self).__init__()
-        self.paths = paths
-        self.cache = cache or NullCache()
-        self.fixups = []
+        self._paths = paths
+        self._cache = cache or NullCache()
+        self._fixups = []
         self.theme_config = theme_config
         # Set the values after we set the rest, since our validation needs
         # our attributes.
         if values:
             self.setAll(values, validate=validate)
 
-    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 addFixup(self, f):
+        self._ensureNotLoaded()
+        self._fixups.append(f)
+
+    def addPath(self, p, first=False):
+        self._ensureNotLoaded()
+        if not first:
+            self._paths.append(p)
+        else:
+            self._paths.insert(0, p)
+
+    def addVariant(self, variant_path, raise_if_not_found=True):
+        self._ensureNotLoaded()
+        if os.path.isfile(variant_path):
+            self.addPath(variant_path)
+        else:
+            name, _ = os.path.splitext(os.path.basename(variant_path))
+            fixup = _make_variant_fixup(name, raise_if_not_found)
+            self.addFixup(fixup)
+
+            logger.warning(
+                "Configuration variants should now be `.yml` files located "
+                "in the `configs/` directory of your website.")
+            logger.warning(
+                "Variants defined in the site configuration will be "
+                "deprecated in a future version of PieCrust.")
+
+    def addVariantValue(self, path, value):
+        def _fixup(index, config):
+            set_dict_value(config, path, value)
+        self.addFixup(_fixup)
+
+    def _ensureNotLoaded(self):
+        if self._values is not None:
+            raise Exception("The configurations has been loaded.")
 
     def _load(self):
-        if self.paths is None:
+        if self._paths is None:
             self._values = self._validateAll({})
             return
 
-        path_times = [os.path.getmtime(p) for p in self.paths]
+        path_times = [os.path.getmtime(p) for p in self._paths]
 
         cache_key_hash = hashlib.md5(
                 ("version=%s&cache=%d" % (
                     APP_VERSION, CACHE_VERSION)).encode('utf8'))
-        for p in self.paths:
+        for p in self._paths:
             cache_key_hash.update(("&path=%s" % p).encode('utf8'))
         cache_key = cache_key_hash.hexdigest()
 
-        if self.cache.isValid('config.json', path_times):
+        if self._cache.isValid('config.json', path_times):
             logger.debug("Loading configuration from cache...")
-            config_text = self.cache.read('config.json')
+            config_text = self._cache.read('config.json')
             self._values = json.loads(
                     config_text,
                     object_pairs_hook=collections.OrderedDict)
@@ -82,32 +127,32 @@
             logger.debug("Outdated cache key '%s' (expected '%s')." % (
                     actual_cache_key, cache_key))
 
-        logger.debug("Loading configuration from: %s" % self.paths)
+        logger.debug("Loading configuration from: %s" % self._paths)
         values = {}
         try:
-            for i, p in enumerate(self.paths):
+            for i, p in enumerate(self._paths):
                 with open(p, 'r', encoding='utf-8') as fp:
                     loaded_values = yaml.load(
                             fp.read(),
                             Loader=ConfigurationLoader)
                 if loaded_values is None:
                     loaded_values = {}
-                for fixup in self.fixups:
+                for fixup in self._fixups:
                     fixup(i, loaded_values)
                 merge_dicts(values, loaded_values)
 
-            for fixup in self.fixups:
-                fixup(len(self.paths), values)
+            for fixup in self._fixups:
+                fixup(-1, values)
 
             self._values = self._validateAll(values)
         except Exception as ex:
             raise Exception("Error loading configuration from: %s" %
-                            ', '.join(self.paths)) from ex
+                            ', '.join(self._paths)) from ex
 
         logger.debug("Caching configuration...")
         self._values['__cache_key'] = cache_key
         config_text = json.dumps(self._values)
-        self.cache.write('config.json', config_text)
+        self._cache.write('config.json', config_text)
 
         self._values['__cache_valid'] = False
 
--- a/piecrust/baking/baker.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/baking/baker.py	Thu Mar 03 08:22:41 2016 -0800
@@ -540,19 +540,27 @@
             logger.error("  " + e)
 
     def _createWorkerPool(self, previous_record_path):
+        from piecrust.app import PieCrustFactory
         from piecrust.workerpool import WorkerPool
         from piecrust.baking.worker import BakeWorkerContext, BakeWorker
 
+        appfactory = PieCrustFactory(
+                self.app.root_dir,
+                cache=self.app.cache.enabled,
+                cache_key=self.app.cache_key,
+                config_variant=self.applied_config_variant,
+                config_values=self.applied_config_values,
+                debug=self.app.debug,
+                theme_site=self.app.theme_site)
+
         worker_count = self.app.config.get('baker/workers')
         batch_size = self.app.config.get('baker/batch_size')
 
         ctx = BakeWorkerContext(
-                self.app.root_dir, self.app.cache.base_dir, self.out_dir,
-                previous_record_path=previous_record_path,
-                config_variant=self.applied_config_variant,
-                config_values=self.applied_config_values,
-                force=self.force, debug=self.app.debug,
-                theme_site=self.app.theme_site)
+                appfactory,
+                self.out_dir,
+                force=self.force,
+                previous_record_path=previous_record_path)
         pool = WorkerPool(
                 worker_count=worker_count,
                 batch_size=batch_size,
--- a/piecrust/baking/worker.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/baking/worker.py	Thu Mar 03 08:22:41 2016 -0800
@@ -15,19 +15,12 @@
 
 
 class BakeWorkerContext(object):
-    def __init__(self, root_dir, sub_cache_dir, out_dir, *,
-                 previous_record_path=None,
-                 config_variant=None, config_values=None,
-                 force=False, debug=False, theme_site=False):
-        self.root_dir = root_dir
-        self.sub_cache_dir = sub_cache_dir
+    def __init__(self, appfactory, out_dir, *,
+                 force=False, previous_record_path=None):
+        self.appfactory = appfactory
         self.out_dir = out_dir
+        self.force = force
         self.previous_record_path = previous_record_path
-        self.config_variant = config_variant
-        self.config_values = config_values
-        self.force = force
-        self.debug = debug
-        self.theme_site = theme_site
         self.app = None
         self.previous_record = None
         self.previous_record_index = None
@@ -40,9 +33,7 @@
 
     def initialize(self):
         # Create the app local to this worker.
-        app = PieCrust(self.ctx.root_dir, debug=self.ctx.debug,
-                       theme_site=self.ctx.theme_site)
-        app._useSubCacheDir(self.ctx.sub_cache_dir)
+        app = self.ctx.appfactory.create()
         app.config.set('baker/is_baking', True)
         app.config.set('baker/worker_id', self.wid)
         app.env.base_asset_url_format = '%uri%'
@@ -50,8 +41,6 @@
         app.env.registerTimer("BakeWorker_%d_Total" % self.wid)
         app.env.registerTimer("BakeWorkerInit")
         app.env.registerTimer("JobReceive")
-        apply_variant_and_values(app, self.ctx.config_variant,
-                                 self.ctx.config_values)
         self.ctx.app = app
 
         # Load previous record
@@ -139,7 +128,7 @@
         except Exception as ex:
             logger.debug("Got loading error. Sending it to master.")
             result['errors'] = _get_errors(ex)
-            if self.ctx.debug:
+            if self.ctx.app.debug:
                 logger.exception(ex)
         return result
 
@@ -223,7 +212,7 @@
         except BakingError as ex:
             logger.debug("Got baking error. Sending it to master.")
             result['errors'] = _get_errors(ex)
-            if self.ctx.debug:
+            if self.ctx.app.debug:
                 logger.exception(ex)
 
         return result
--- a/piecrust/commands/builtin/baking.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/commands/builtin/baking.py	Thu Mar 03 08:22:41 2016 -0800
@@ -109,7 +109,9 @@
     def _bakeAssets(self, ctx, out_dir):
         proc = ProcessorPipeline(
                 ctx.app, out_dir,
-                force=ctx.args.force)
+                force=ctx.args.force,
+                applied_config_variant=ctx.config_variant,
+                applied_config_values=ctx.config_values)
         record = proc.run()
         _merge_timers(record.timers, ctx.timers)
         return record.success
--- a/piecrust/commands/builtin/serving.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/commands/builtin/serving.py	Thu Mar 03 08:22:41 2016 -0800
@@ -42,12 +42,19 @@
         port = int(ctx.args.port)
         debug = ctx.args.debug or ctx.args.use_debugger
 
+        from piecrust.app import PieCrustFactory
+        appfactory = PieCrustFactory(
+                ctx.app.root_dir,
+                cache=ctx.app.cache.enabled,
+                cache_key=ctx.app.cache_key,
+                config_variant=ctx.config_variant,
+                config_values=ctx.config_values,
+                debug=ctx.app.debug,
+                theme_site=ctx.app.theme_site)
+
         if ctx.args.wsgi == 'werkzeug':
             run_werkzeug_server(
-                    root_dir, host, port,
-                    debug_piecrust=debug,
-                    theme_site=ctx.args.theme,
-                    sub_cache_dir=ctx.app.sub_cache_dir,
+                    appfactory, host, port,
                     use_debugger=debug,
                     use_reloader=ctx.args.use_reloader)
 
@@ -60,10 +67,5 @@
                 options['loglevel'] = 'debug'
             if ctx.args.use_reloader:
                 options['reload'] = True
-            run_gunicorn_server(
-                    root_dir,
-                    debug_piecrust=debug,
-                    theme_site=ctx.args.theme,
-                    sub_cache_dir=ctx.app.sub_cache_dir,
-                    gunicorn_options=options)
+            run_gunicorn_server(appfactory, gunicorn_options=options)
 
--- a/piecrust/commands/builtin/util.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/commands/builtin/util.py	Thu Mar 03 08:22:41 2016 -0800
@@ -63,7 +63,7 @@
         pass
 
     def run(self, ctx):
-        cache_dir = ctx.app.sub_cache_dir
+        cache_dir = ctx.app.cache_dir
         if cache_dir and os.path.isdir(cache_dir):
             logger.info("Purging cache: %s" % cache_dir)
             shutil.rmtree(cache_dir)
--- a/piecrust/configuration.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/configuration.py	Thu Mar 03 08:22:41 2016 -0800
@@ -28,28 +28,15 @@
 
     def __getitem__(self, key):
         self._ensureLoaded()
-        bits = key.split('/')
-        cur = self._values
-        for b in bits:
-            try:
-                cur = cur[b]
-            except KeyError:
-                raise KeyError("No such item: %s" % key)
-        return cur
+        try:
+            return get_dict_value(self._values, key)
+        except KeyError:
+            raise KeyError("No such item: %s" % key)
 
     def __setitem__(self, key, value):
         self._ensureLoaded()
         value = self._validateValue(key, value)
-        bits = key.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]
+        set_dict_value(self._values, key, value)
 
     def __delitem__(self, key):
         raise NotImplementedError()
@@ -129,6 +116,27 @@
         return value
 
 
+def get_dict_value(d, key):
+    bits = key.split('/')
+    cur = d
+    for b in bits:
+        cur = cur[b]
+    return cur
+
+
+def set_dict_value(d, key, value):
+    bits = key.split('/')
+    bitslen = len(bits)
+    cur = d
+    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 merge_dicts(source, merging, validator=None, *args):
     _recurse_merge_dicts(source, merging, None, validator)
     for other in args:
--- a/piecrust/environment.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/environment.py	Thu Mar 03 08:22:41 2016 -0800
@@ -89,7 +89,9 @@
         self.was_cache_cleaned = False
         self.base_asset_url_format = '%uri%'
 
-        self._onSubCacheDirChanged(app)
+        for name, repo in self.fs_caches.items():
+            cache = app.cache.getCache(name)
+            repo.fs_cache = cache
 
     def registerTimer(self, category, *, raise_if_registered=True):
         if raise_if_registered and category in self._timers:
@@ -109,11 +111,6 @@
     def stepTimerSince(self, category, since):
         self.stepTimer(category, time.perf_counter() - since)
 
-    def _onSubCacheDirChanged(self, app):
-        for name, repo in self.fs_caches.items():
-            cache = app.cache.getCache(name)
-            repo.fs_cache = cache
-
 
 class StandardEnvironment(Environment):
     def __init__(self):
--- a/piecrust/main.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/main.py	Thu Mar 03 08:22:41 2016 -0800
@@ -3,6 +3,7 @@
 import io
 import sys
 import time
+import hashlib
 import logging
 import argparse
 import colorama
@@ -51,9 +52,6 @@
         self.plugin_loader = PluginLoader(self)
         self.env = None
 
-    def useSubCache(self, cache_name, cache_key):
-        pass
-
 
 def main():
     if sys.platform == 'darwin':
@@ -135,6 +133,15 @@
             help="Write a PID file for the current process.")
 
 
+""" Kinda hacky, but we want the `serve` command to use a different cache
+    so that PieCrust doesn't need to re-render all the pages when going
+    between `serve` and `bake` (or, worse, *not* re-render them all correctly
+    and end up serving or baking the wrong version).
+"""
+_command_caches = {
+        'serve': 'server'}
+
+
 def _pre_parse_chef_args(argv):
     # We need to parse some arguments before we can build the actual argument
     # parser, because it can affect which plugins will be loaded. Also, log-
@@ -142,7 +149,7 @@
     # from the beginning.
     parser = argparse.ArgumentParser()
     _setup_main_parser_arguments(parser)
-    parser.add_argument('args', nargs=argparse.REMAINDER)
+    parser.add_argument('extra_args', nargs=argparse.REMAINDER)
     res, _ = parser.parse_known_args(argv)
 
     # Setup the logger.
@@ -196,6 +203,23 @@
     return res
 
 
+def _build_cache_key(pre_args):
+    cache_key_str = 'default'
+    if pre_args.extra_args:
+        cmd_name = pre_args.extra_args[0]
+        if cmd_name in _command_caches:
+            cache_key_str = _command_caches[cmd_name]
+    if pre_args.config_variant is not None:
+        cache_key_str += ',variant=%s' % pre_args.config_variant
+    if pre_args.config_values:
+        for name, value in pre_args.config_values:
+            cache_key_str += ',%s=%s' % (name, value)
+
+    logger.debug("Using cache key: %s" % cache_key_str)
+    cache_key = hashlib.md5(cache_key_str.encode('utf8')).hexdigest()
+    return cache_key
+
+
 def _run_chef(pre_args, argv):
     # Setup the app.
     start_time = time.perf_counter()
@@ -208,33 +232,27 @@
         except SiteNotFoundError:
             root = None
 
-    if not root:
-        app = NullPieCrust(
-                theme_site=pre_args.theme)
-    else:
+    # Can't apply custom configuration stuff if there's no website.
+    if (pre_args.config_variant or pre_args.config_values) and not root:
+        raise SiteNotFoundError(
+                "Can't apply any configuration variant or value overrides, "
+                "there is no website here.")
+
+    if root:
+        cache_key = None
+        if not pre_args.no_cache:
+            cache_key = _build_cache_key(pre_args)
         app = PieCrust(
                 root,
                 theme_site=pre_args.theme,
                 cache=(not pre_args.no_cache),
+                cache_key=cache_key,
                 debug=pre_args.debug)
-
-    # Build a hash for a custom cache directory.
-    cache_key = 'default'
-
-    # Handle custom configurations.
-    if (pre_args.config_variant or pre_args.config_values) and not root:
-        raise SiteNotFoundError(
-                "Can't apply any configuration variant or value overrides, "
-                "there is no website here.")
-    apply_variant_and_values(app, pre_args.config_variant,
-                             pre_args.config_values)
-
-    # Adjust the cache key.
-    if pre_args.config_variant is not None:
-        cache_key += ',variant=%s' % pre_args.config_variant
-    if pre_args.config_values:
-        for name, value in pre_args.config_values:
-            cache_key += ',%s=%s' % (name, value)
+        apply_variant_and_values(
+                app, pre_args.config_variant, pre_args.config_values)
+    else:
+        app = NullPieCrust(
+                theme_site=pre_args.theme)
 
     # Setup the arg parser.
     parser = argparse.ArgumentParser(
@@ -270,10 +288,6 @@
         parser.print_help()
         return 0
 
-    # Use a customized cache for the command and current config.
-    if result.cache_name != 'default' or cache_key != 'default':
-        app.useSubCache(result.cache_name, cache_key)
-
     # Run the command!
     ctx = CommandContext(app, parser, result)
     ctx.config_variant = pre_args.config_variant
--- a/piecrust/processing/pipeline.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/processing/pipeline.py	Thu Mar 03 08:22:41 2016 -0800
@@ -27,13 +27,17 @@
 
 
 class ProcessorPipeline(object):
-    def __init__(self, app, out_dir, force=False):
+    def __init__(self, app, out_dir, force=False,
+                 applied_config_variant=None,
+                 applied_config_values=None):
         assert app and out_dir
         self.app = app
         self.out_dir = out_dir
         self.force = force
+        self.applied_config_variant = applied_config_variant
+        self.applied_config_values = applied_config_values
 
-        tmp_dir = app.sub_cache_dir
+        tmp_dir = app.cache_dir
         if not tmp_dir:
             import tempfile
             tmp_dir = os.path.join(tempfile.gettempdir(), 'piecrust')
@@ -246,14 +250,24 @@
         ctx.jobs.append(job)
 
     def _createWorkerPool(self):
+        from piecrust.app import PieCrustFactory
         from piecrust.workerpool import WorkerPool
         from piecrust.processing.worker import (
                 ProcessingWorkerContext, ProcessingWorker)
 
+        appfactory = PieCrustFactory(
+                self.app.root_dir,
+                cache=self.app.cache.enabled,
+                cache_key=self.app.cache_key,
+                config_variant=self.applied_config_variant,
+                config_values=self.applied_config_values,
+                debug=self.app.debug,
+                theme_site=self.app.theme_site)
+
         ctx = ProcessingWorkerContext(
-                self.app.root_dir, self.out_dir, self.tmp_dir,
-                force=self.force, debug=self.app.debug,
-                theme_site=self.app.theme_site)
+                appfactory,
+                self.out_dir, self.tmp_dir,
+                force=self.force)
         ctx.enabled_processors = self.enabled_processors
         if self.additional_processors_factories is not None:
             ctx.additional_processors = [
--- a/piecrust/processing/sass.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/processing/sass.py	Thu Mar 03 08:22:41 2016 -0800
@@ -129,7 +129,7 @@
 
         cache_dir = None
         if self.app.cache.enabled:
-            cache_dir = os.path.join(self.app.sub_cache_dir, 'sass')
+            cache_dir = os.path.join(self.app.cache_dir, 'sass')
         self._conf.setdefault('cache_dir', cache_dir)
 
     def _getMapPath(self, path):
--- a/piecrust/processing/worker.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/processing/worker.py	Thu Mar 03 08:22:41 2016 -0800
@@ -2,7 +2,7 @@
 import os.path
 import time
 import logging
-from piecrust.app import PieCrust
+from piecrust.app import PieCrust, apply_variant_and_values
 from piecrust.processing.base import PipelineContext
 from piecrust.processing.records import (
         FLAG_NONE, FLAG_PREPARED, FLAG_PROCESSED,
@@ -23,14 +23,12 @@
 
 
 class ProcessingWorkerContext(object):
-    def __init__(self, root_dir, out_dir, tmp_dir, *,
-                 force=False, debug=False, theme_site=False):
-        self.root_dir = root_dir
+    def __init__(self, appfactory, out_dir, tmp_dir, *,
+                 force=False):
+        self.appfactory = appfactory
         self.out_dir = out_dir
         self.tmp_dir = tmp_dir
         self.force = force
-        self.debug = debug
-        self.theme_site = theme_site
         self.is_profiling = False
         self.enabled_processors = None
         self.additional_processors = None
@@ -60,8 +58,7 @@
 
     def initialize(self):
         # Create the app local to this worker.
-        app = PieCrust(self.ctx.root_dir, debug=self.ctx.debug,
-                       theme_site=self.ctx.theme_site)
+        app = self.ctx.appfactory.create()
         app.env.registerTimer("PipelineWorker_%d_Total" % self.wid)
         app.env.registerTimer("PipelineWorkerInit")
         app.env.registerTimer("JobReceive")
--- a/piecrust/serving/middlewares.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/serving/middlewares.py	Thu Mar 03 08:22:41 2016 -0800
@@ -40,18 +40,14 @@
 class PieCrustDebugMiddleware(object):
     """ WSGI middleware that handles debugging of PieCrust stuff.
     """
-    def __init__(self, app, root_dir, debug=False, theme_site=False,
-                 sub_cache_dir=None, run_sse_check=None):
+    def __init__(self, app, appfactory,
+                 run_sse_check=None):
         self.app = app
-        self.root_dir = root_dir
-        self.debug = debug
-        self.theme_site = theme_site
-        self.sub_cache_dir = sub_cache_dir
+        self.appfactory = appfactory
         self.run_sse_check = run_sse_check
         self._proc_loop = None
-        self._out_dir = os.path.join(root_dir, CACHE_DIR, 'server')
-        if sub_cache_dir:
-            self._out_dir = os.path.join(sub_cache_dir, 'server')
+        self._out_dir = os.path.join(
+                root_dir, CACHE_DIR, appfactory.cache_key, 'server')
         self._handlers = {
                 'debug_info': self._getDebugInfo,
                 'werkzeug_shutdown': self._shutdownWerkzeug,
@@ -63,10 +59,7 @@
             # to start the pipeline loop in the inner process most of the
             # time so we let the implementation tell us if this is OK.
             from piecrust.serving.procloop import ProcessingLoop
-            self._proc_loop = ProcessingLoop(root_dir, self._out_dir,
-                                             theme_site=theme_site,
-                                             sub_cache_dir=sub_cache_dir,
-                                             debug=debug)
+            self._proc_loop = ProcessingLoop(self.appfactory, self._out_dir)
             self._proc_loop.start()
 
     def __call__(self, environ, start_response):
@@ -82,8 +75,7 @@
         return self.app(environ, start_response)
 
     def _getDebugInfo(self, request, start_response):
-        app = get_app_for_server(self.root_dir, debug=self.debug,
-                                 sub_cache_dir=self.sub_cache_dir)
+        app = get_app_for_server(self.appfactory)
         if not app.config.get('site/enable_debug_info'):
             return Forbidden()
 
--- a/piecrust/serving/procloop.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/serving/procloop.py	Thu Mar 03 08:22:41 2016 -0800
@@ -75,15 +75,10 @@
 
 
 class ProcessingLoop(threading.Thread):
-    def __init__(self, root_dir, out_dir, sub_cache_dir=None,
-                 theme_site=False, debug=False):
+    def __init__(self, appfactory):
         super(ProcessingLoop, self).__init__(
                 name='pipeline-reloader', daemon=True)
-        self.root_dir = root_dir
-        self.out_dir = out_dir
-        self.sub_cache_dir = sub_cache_dir
-        self.debug = debug
-        self.theme_site = theme_site
+        self.appfactory = appfactory
         self.last_status_id = 0
         self.interval = 1
         self.app = None
@@ -95,7 +90,7 @@
         self._last_config_mtime = 0
         self._obs = []
         self._obs_lock = threading.Lock()
-        if theme_site:
+        if appfactory.theme_site:
             self._config_path = os.path.join(root_dir, THEME_CONFIG_PATH)
         else:
             self._config_path = os.path.join(root_dir, CONFIG_PATH)
@@ -162,10 +157,7 @@
 
     def _initPipeline(self):
         # Create the app and pipeline.
-        self.app = PieCrust(root_dir=self.root_dir, debug=self.debug,
-                            theme_site=self.theme_site)
-        if self.sub_cache_dir:
-            self.app._useSubCacheDir(self.sub_cache_dir)
+        self.app = self.appfactory.create()
         self.pipeline = ProcessorPipeline(self.app, self.out_dir)
 
         # Get the list of assets directories.
--- a/piecrust/serving/server.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/serving/server.py	Thu Mar 03 08:22:41 2016 -0800
@@ -22,8 +22,8 @@
 
 
 class WsgiServer(object):
-    def __init__(self, root_dir, **kwargs):
-        self.server = Server(root_dir, **kwargs)
+    def __init__(self, appfactory, **kwargs):
+        self.server = Server(appfactory, **kwargs)
 
     def __call__(self, environ, start_response):
         return self.server._run_request(environ, start_response)
@@ -70,21 +70,20 @@
 
 
 class Server(object):
-    def __init__(self, root_dir,
-                 debug=False, theme_site=False,
-                 sub_cache_dir=None, enable_debug_info=True,
-                 root_url='/', static_preview=True):
-        self.root_dir = root_dir
-        self.debug = debug
-        self.theme_site = theme_site
-        self.sub_cache_dir = sub_cache_dir
+    def __init__(self, appfactory,
+                 enable_debug_info=True,
+                 root_url='/',
+                 static_preview=True):
+        self.appfactory = appfactory
         self.enable_debug_info = enable_debug_info
         self.root_url = root_url
         self.static_preview = static_preview
         self._page_record = ServeRecord()
-        self._out_dir = os.path.join(root_dir, CACHE_DIR, 'server')
-        if sub_cache_dir:
-            self._out_dir = os.path.join(sub_cache_dir, 'server')
+        self._out_dir = os.path.join(
+                appfactory.root_dir,
+                CACHE_DIR,
+                (appfactory.cache_key or 'default'),
+                'server')
 
     def _run_request(self, environ, start_response):
         try:
@@ -111,9 +110,7 @@
             return response
 
         # Create the app for this request.
-        app = get_app_for_server(self.root_dir, debug=self.debug,
-                                 theme_site=self.theme_site,
-                                 sub_cache_dir=self.sub_cache_dir,
+        app = get_app_for_server(self.appfactory,
                                  root_url=self.root_url)
         if (app.config.get('site/enable_debug_info') and
                 self.enable_debug_info and
--- a/piecrust/serving/util.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/serving/util.py	Thu Mar 03 08:22:41 2016 -0800
@@ -5,7 +5,7 @@
 import datetime
 from werkzeug.wrappers import Response
 from werkzeug.wsgi import wrap_file
-from piecrust.app import PieCrust
+from piecrust.app import PieCrust, apply_variant_and_values
 from piecrust.rendering import QualifiedPage
 from piecrust.routing import RouteNotFoundError
 from piecrust.sources.base import MODE_PARSING
@@ -16,11 +16,8 @@
 logger = logging.getLogger(__name__)
 
 
-def get_app_for_server(root_dir, debug=False, theme_site=False,
-                       sub_cache_dir=None, root_url='/'):
-    app = PieCrust(root_dir=root_dir, debug=debug, theme_site=theme_site)
-    if sub_cache_dir:
-        app._useSubCacheDir(sub_cache_dir)
+def get_app_for_server(appfactory, root_url='/'):
+    app = appfactory.create()
     app.config.set('site/root', root_url)
     app.config.set('server/is_serving', True)
     return app
--- a/piecrust/serving/wrappers.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/serving/wrappers.py	Thu Mar 03 08:22:41 2016 -0800
@@ -8,9 +8,7 @@
 logger = logging.getLogger(__name__)
 
 
-def run_werkzeug_server(root_dir, host, port,
-                        debug_piecrust=False, theme_site=False,
-                        sub_cache_dir=None,
+def run_werkzeug_server(appfactory, host, port,
                         use_debugger=False, use_reloader=False):
     from werkzeug.serving import run_simple
 
@@ -24,10 +22,7 @@
         return (not use_reloader or
                 os.environ.get('WERKZEUG_RUN_MAIN') == 'true')
 
-    app = _get_piecrust_server(root_dir,
-                               debug=debug_piecrust,
-                               theme_site=theme_site,
-                               sub_cache_dir=sub_cache_dir,
+    app = _get_piecrust_server(appfactory,
                                run_sse_check=_run_sse_check)
 
     # We need to do a few things to get Werkzeug to properly shutdown or
@@ -79,10 +74,7 @@
         raise
 
 
-def run_gunicorn_server(root_dir,
-                        debug_piecrust=False, theme_site=False,
-                        sub_cache_dir=None,
-                        gunicorn_options=None):
+def run_gunicorn_server(appfactory, gunicorn_options=None):
     from gunicorn.app.base import BaseApplication
 
     class PieCrustGunicornApplication(BaseApplication):
@@ -99,28 +91,20 @@
         def load(self):
             return self.app
 
-    app = _get_piecrust_server(root_dir,
-                               debug=debug_piecrust,
-                               theme_site=theme_site,
-                               sub_cache_dir=sub_cache_dir)
+    app = _get_piecrust_server(appfactory)
 
     gunicorn_options = gunicorn_options or {}
     app_wrapper = PieCrustGunicornApplication(app, gunicorn_options)
     app_wrapper.run()
 
 
-def _get_piecrust_server(
-        root_dir, debug=False, theme_site=False,
-        sub_cache_dir=None, run_sse_check=None):
+def _get_piecrust_server(appfactory, run_sse_check=None):
     from piecrust.serving.middlewares import (
             StaticResourcesMiddleware, PieCrustDebugMiddleware)
     from piecrust.serving.server import WsgiServer
-    app = WsgiServer(root_dir, debug=debug, theme_site=theme_site,
-                     sub_cache_dir=sub_cache_dir)
+    app = WsgiServer(appfactory)
     app = StaticResourcesMiddleware(app)
-    app = PieCrustDebugMiddleware(app, root_dir,
-                                  theme_site=theme_site,
-                                  sub_cache_dir=sub_cache_dir,
-                                  run_sse_check=run_sse_check)
+    app = PieCrustDebugMiddleware(
+            app, appfactory, run_sse_check=run_sse_check)
     return app
 
--- a/piecrust/wsgiutil/__init__.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/piecrust/wsgiutil/__init__.py	Thu Mar 03 08:22:41 2016 -0800
@@ -1,9 +1,9 @@
 from piecrust.serving.server import WsgiServer
 
 
-def get_app(root_dir, sub_cache_dir='prod', enable_debug_info=False):
+def get_app(root_dir, cache_key='prod', enable_debug_info=False):
     app = WsgiServer(root_dir,
-                     sub_cache_dir=sub_cache_dir,
+                     cache_key=cache_key,
                      enable_debug_info=enable_debug_info)
     return app
 
--- a/tests/bakes/test_variant.yaml	Thu Mar 03 08:19:28 2016 -0800
+++ b/tests/bakes/test_variant.yaml	Thu Mar 03 08:22:41 2016 -0800
@@ -12,6 +12,15 @@
 ---
 config:
     what: not good
+config_variant: test
+in:
+    pages/_index.md: 'This is {{what}}.'
+    configs/test.yml: 'what: awesome'
+out:
+    index.html: 'This is awesome.'
+---
+config:
+    what: not good
 config_values:
     what: awesome
 in:
--- a/tests/conftest.py	Thu Mar 03 08:19:28 2016 -0800
+++ b/tests/conftest.py	Thu Mar 03 08:22:41 2016 -0800
@@ -322,9 +322,11 @@
 
         from werkzeug.test import Client
         from werkzeug.wrappers import BaseResponse
+        from piecrust.app import PieCrustFactory
         from piecrust.serving.server import Server
         with mock_fs_scope(fs):
-            server = Server(fs.path('/kitchen'))
+            appfactory = PieCrustFactory(fs.path('/kitchen'))
+            server = Server(appfactory)
             test_app = self._TestApp(server)
             client = Client(test_app, BaseResponse)
             resp = client.get(url)