changeset 371:c2ca72fb7f0b 2.0.0a8

caching: Use separate caches for config variants and other contexts. * The `_cache` directory is now organized in multiple "sub-caches" for different contexts. * A new context is created when config variants or overrides are applied. * `serve` context uses a different context that the other commends, to prevent the `bake` command's output from messing up the preview server (e.g. with how asset URLs are generated differently between the two). * Fix a few places where the cache directory was referenced directly.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 03 May 2015 23:59:46 -0700
parents a1bbe66cba03
children 115668bb447d
files piecrust/app.py piecrust/baking/baker.py piecrust/cache.py piecrust/commands/base.py piecrust/commands/builtin/serving.py piecrust/commands/builtin/util.py piecrust/environment.py piecrust/main.py piecrust/processing/base.py piecrust/processing/sass.py piecrust/serving.py
diffstat 11 files changed, 156 insertions(+), 66 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/app.py	Sun May 03 23:45:32 2015 -0700
+++ b/piecrust/app.py	Sun May 03 23:59:46 2015 -0700
@@ -403,7 +403,8 @@
         self.plugin_loader = PluginLoader(self)
 
         if cache:
-            self.cache = ExtensibleCache(self.cache_dir)
+            cache_dir = os.path.join(self.cache_dir, 'default')
+            self.cache = ExtensibleCache(cache_dir)
         else:
             self.cache = NullExtensibleCache()
 
@@ -494,6 +495,12 @@
     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
+
     @cached_property
     def sources(self):
         defs = {}
@@ -557,6 +564,18 @@
                 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):
--- a/piecrust/baking/baker.py	Sun May 03 23:45:32 2015 -0700
+++ b/piecrust/baking/baker.py	Sun May 03 23:59:46 2015 -0700
@@ -1,6 +1,5 @@
 import time
 import os.path
-import shutil
 import hashlib
 import logging
 import threading
@@ -10,7 +9,6 @@
 from piecrust.baking.single import (BakingError, PageBaker)
 from piecrust.chefutil import format_timed, log_friendly_exception
 from piecrust.sources.base import (
-        PageFactory,
         REALM_NAMES, REALM_USER, REALM_THEME)
 
 
@@ -141,12 +139,7 @@
 
         if reason is not None:
             # We have to bake everything from scratch.
-            for cache_name in self.app.cache.getCacheNames(
-                    except_names=['app']):
-                cache_dir = self.app.cache.getCacheDir(cache_name)
-                if os.path.isdir(cache_dir):
-                    logger.debug("Cleaning baker cache: %s" % cache_dir)
-                    shutil.rmtree(cache_dir)
+            self.app.cache.clearCaches(except_names=['app'])
             self.force = True
             record.incremental_count = 0
             record.clearPrevious()
--- a/piecrust/cache.py	Sun May 03 23:45:32 2015 -0700
+++ b/piecrust/cache.py	Sun May 03 23:59:46 2015 -0700
@@ -1,5 +1,6 @@
 import os
 import os.path
+import shutil
 import codecs
 import logging
 import threading
@@ -41,6 +42,16 @@
             return dirnames
         return [dn for dn in dirnames if dn not in except_names]
 
+    def clearCache(self, name):
+        cache_dir = self.getCacheDir(name)
+        if os.path.isdir(cache_dir):
+            logger.debug("Cleaning cache: %s" % cache_dir)
+            shutil.rmtree(cache_dir)
+
+    def clearCaches(self, except_names=None):
+        for name in self.getCacheNames(except_names=except_names):
+            self.clearCache(name)
+
 
 class SimpleCache(object):
     def __init__(self, base_dir):
@@ -122,3 +133,15 @@
     def getCache(self, name):
         return self.null_cache
 
+    def getCacheDir(self, name):
+        raise NotImplementedError()
+
+    def getCacheNames(self, except_names=None):
+        return []
+
+    def clearCache(self, name):
+        pass
+
+    def clearCaches(self, except_names=None):
+        pass
+
--- a/piecrust/commands/base.py	Sun May 03 23:45:32 2015 -0700
+++ b/piecrust/commands/base.py	Sun May 03 23:59:46 2015 -0700
@@ -19,6 +19,7 @@
         self.name = '__unknown__'
         self.description = '__unknown__'
         self.requires_website = True
+        self.cache_name = 'default'
 
     def setupParser(self, parser, app):
         raise NotImplementedError()
--- a/piecrust/commands/builtin/serving.py	Sun May 03 23:45:32 2015 -0700
+++ b/piecrust/commands/builtin/serving.py	Sun May 03 23:59:46 2015 -0700
@@ -11,6 +11,7 @@
         super(ServeCommand, self).__init__()
         self.name = 'serve'
         self.description = "Runs a local web server to serve your website."
+        self.cache_name = 'server'
 
     def setupParser(self, parser, app):
         parser.add_argument(
@@ -43,6 +44,7 @@
         server = Server(
                 ctx.app.root_dir,
                 debug=debug,
+                sub_cache_dir=ctx.app.sub_cache_dir,
                 use_reloader=ctx.args.use_reloader)
         app = server.getWsgiApp()
 
--- a/piecrust/commands/builtin/util.py	Sun May 03 23:45:32 2015 -0700
+++ b/piecrust/commands/builtin/util.py	Sun May 03 23:59:46 2015 -0700
@@ -59,8 +59,8 @@
         pass
 
     def run(self, ctx):
-        cache_dir = ctx.app.cache_dir
-        if os.path.isdir(cache_dir):
+        cache_dir = ctx.app.sub_cache_dir
+        if cache_dir and os.path.isdir(cache_dir):
             logger.info("Purging cache: %s" % cache_dir)
             shutil.rmtree(cache_dir)
 
--- a/piecrust/environment.py	Sun May 03 23:45:32 2015 -0700
+++ b/piecrust/environment.py	Sun May 03 23:59:46 2015 -0700
@@ -137,7 +137,9 @@
         self.exec_info_stack.clear()
         self.was_cache_cleaned = False
         self.base_asset_url_format = '%uri%'
+        self._onSubCacheDirChanged(app)
 
+    def _onSubCacheDirChanged(self, app):
         for name, repo in self.fs_caches.items():
             cache = app.cache.getCache(name)
             repo.fs_cache = cache
--- a/piecrust/main.py	Sun May 03 23:45:32 2015 -0700
+++ b/piecrust/main.py	Sun May 03 23:59:46 2015 -0700
@@ -181,14 +181,19 @@
     else:
         app = PieCrust(root, cache=pre_args.cache, debug=pre_args.debug)
 
+    # Build a hash for a custom cache directory.
+    cache_key = 'default'
+
     # Handle a configuration variant.
     if pre_args.config_variant is not None:
         if not root:
             raise SiteNotFoundError("Can't apply any variant.")
         app.config.applyVariant('variants/' + pre_args.config_variant)
+        cache_key += ',variant=%s' % pre_args.config_variant
     for name, value in pre_args.config_values:
         logger.debug("Setting configuration '%s' to: %s" % (name, value))
         app.config.set(name, value)
+        cache_key += ',%s=%s' % (name, value)
 
     # Setup the arg parser.
     parser = argparse.ArgumentParser(
@@ -234,6 +239,7 @@
         p = subparsers.add_parser(c.name, help=c.description)
         c.setupParser(p, app)
         p.set_defaults(func=c.checkedRun)
+        p.set_defaults(cache_name=c.cache_name)
 
     help_cmd = next(filter(lambda c: c.name == 'help', commands), None)
     if help_cmd and help_cmd.has_topics:
@@ -253,6 +259,10 @@
         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)
     exit_code = result.func(ctx)
--- a/piecrust/processing/base.py	Sun May 03 23:45:32 2015 -0700
+++ b/piecrust/processing/base.py	Sun May 03 23:59:46 2015 -0700
@@ -132,7 +132,7 @@
         self.out_dir = out_dir
         self.force = force
 
-        tmp_dir = app.cache_dir
+        tmp_dir = app.sub_cache_dir
         if not tmp_dir:
             import tempfile
             tmp_dir = os.path.join(tempfile.gettempdir(), 'piecrust')
--- a/piecrust/processing/sass.py	Sun May 03 23:45:32 2015 -0700
+++ b/piecrust/processing/sass.py	Sun May 03 23:59:46 2015 -0700
@@ -128,7 +128,7 @@
 
         cache_dir = None
         if self.app.cache.enabled:
-            cache_dir = os.path.join(self.app.cache_dir, 'sass')
+            cache_dir = os.path.join(self.app.sub_cache_dir, 'sass')
         self._conf.setdefault('cache_dir', cache_dir)
 
     def _getMapPath(self, path):
--- a/piecrust/serving.py	Sun May 03 23:45:32 2015 -0700
+++ b/piecrust/serving.py	Sun May 03 23:59:46 2015 -0700
@@ -66,9 +66,11 @@
 
 class Server(object):
     def __init__(self, root_dir,
-                 debug=False, use_reloader=False, static_preview=True):
+                 debug=False, sub_cache_dir=None,
+                 use_reloader=False, static_preview=True):
         self.root_dir = root_dir
         self.debug = debug
+        self.sub_cache_dir = sub_cache_dir
         self.use_reloader = use_reloader
         self.static_preview = static_preview
         self._out_dir = None
@@ -80,7 +82,8 @@
         # Bake all the assets so we know what we have, and so we can serve
         # them to the client. We need a temp app for this.
         app = PieCrust(root_dir=self.root_dir, debug=self.debug)
-        self._out_dir = os.path.join(app.cache_dir, 'server')
+        app._useSubCacheDir(self.sub_cache_dir)
+        self._out_dir = os.path.join(app.sub_cache_dir, 'server')
         self._page_record = ServeRecord()
 
         if (not self.use_reloader or
@@ -129,6 +132,7 @@
 
         # Create the app for this request.
         app = PieCrust(root_dir=self.root_dir, debug=self.debug)
+        app._useSubCacheDir(self.sub_cache_dir)
         app.config.set('site/root', '/')
         app.config.set('server/is_serving', True)
         if (app.config.get('site/enable_debug_info') and
@@ -226,67 +230,35 @@
         if len(routes) == 0:
             raise RouteNotFoundError("Can't find route for: %s" % req_path)
 
-        taxonomy = None
-        tax_terms = None
+        rendered_page = None
+        first_not_found = None
         for route, route_metadata in routes:
-            source = app.getSource(route.source_name)
-            if route.taxonomy_name is None:
-                factory = source.findPageFactory(route_metadata, MODE_PARSING)
-                if factory is not None:
+            try:
+                logger.debug("Trying to render match from source '%s'." %
+                             route.source_name)
+                rendered_page = self._try_render_page(
+                        app, route, route_metadata, page_num, req_path)
+                if rendered_page is not None:
                     break
-            else:
-                taxonomy = app.getTaxonomy(route.taxonomy_name)
-                route_terms = route_metadata.get(taxonomy.term_name)
-                if route_terms is not None:
-                    tax_page_ref = taxonomy.getPageRef(source.name)
-                    factory = tax_page_ref.getFactory()
-                    tax_terms = route.unslugifyTaxonomyTerm(route_terms)
-                    factory.metadata[taxonomy.term_name] = tax_terms
-                    break
+            except NotFound as nfe:
+                if first_not_found is None:
+                    first_not_found = nfe
         else:
             raise SourceNotFoundError(
                     "Can't find path for: %s (looked in: %s)" %
                     (req_path, [r.source_name for r, _ in routes]))
 
-        # Build the page.
-        page = factory.buildPage()
-        # We force the rendering of the page because it could not have
-        # changed, but include pages that did change.
-        qp = QualifiedPage(page, route, route_metadata)
-        render_ctx = PageRenderingContext(qp,
-                                          page_num=page_num,
-                                          force_render=True)
-        if taxonomy is not None:
-            render_ctx.setTaxonomyFilter(taxonomy, tax_terms)
+        # If we haven't found any good match, raise whatever exception we
+        # first got. Otherwise, raise a generic exception.
+        if rendered_page is None:
+            first_not_found = first_not_found or NotFound(
+                    "This page couldn't be found.")
+            raise first_not_found
 
-        # See if this page is known to use sources. If that's the case,
-        # just don't use cached rendered segments for that page (but still
-        # use them for pages that are included in it).
-        entry = self._page_record.getEntry(req_path, page_num)
-        if (taxonomy is not None or entry is None or
-                entry.used_source_names):
-            cache_key = '%s:%s' % (req_path, page_num)
-            app.env.rendered_segments_repository.invalidate(cache_key)
-
-        # Render the page.
-        rendered_page = render_page(render_ctx)
+        # Start doing stuff.
+        page = rendered_page.page
         rp_content = rendered_page.content
 
-        if taxonomy is not None:
-            paginator = rendered_page.data.get('pagination')
-            if (paginator and paginator.is_loaded and
-                    len(paginator.items) == 0):
-                message = ("This URL matched a route for taxonomy '%s' but "
-                           "no pages have been found to have it. This page "
-                           "won't be generated by a bake." % taxonomy.name)
-                raise NotFound(message)
-
-        if entry is None:
-            entry = ServeRecordPageEntry(req_path, page_num)
-            self._page_record.addEntry(entry)
-        for p, pinfo in render_ctx.render_passes.items():
-            entry.used_source_names |= pinfo.used_source_names
-
         # Profiling.
         if app.config.get('site/show_debug_info'):
             now_time = time.clock()
@@ -341,6 +313,74 @@
 
         return response
 
+    def _try_render_page(self, app, route, route_metadata, page_num, req_path):
+        # Match the route to an actual factory.
+        taxonomy_info = None
+        source = app.getSource(route.source_name)
+        if route.taxonomy_name is None:
+            factory = source.findPageFactory(route_metadata, MODE_PARSING)
+            if factory is None:
+                return None
+        else:
+            taxonomy = app.getTaxonomy(route.taxonomy_name)
+            route_terms = route_metadata.get(taxonomy.term_name)
+            if route_terms is None:
+                return None
+
+            tax_page_ref = taxonomy.getPageRef(source.name)
+            factory = tax_page_ref.getFactory()
+            tax_terms = route.unslugifyTaxonomyTerm(route_terms)
+            route_metadata[taxonomy.term_name] = tax_terms
+            taxonomy_info = (taxonomy, tax_terms)
+
+        # Build the page.
+        page = factory.buildPage()
+        # We force the rendering of the page because it could not have
+        # changed, but include pages that did change.
+        qp = QualifiedPage(page, route, route_metadata)
+        render_ctx = PageRenderingContext(qp,
+                                          page_num=page_num,
+                                          force_render=True)
+        if taxonomy_info is not None:
+            taxonomy, tax_terms = taxonomy_info
+            render_ctx.setTaxonomyFilter(taxonomy, tax_terms)
+
+        # See if this page is known to use sources. If that's the case,
+        # just don't use cached rendered segments for that page (but still
+        # use them for pages that are included in it).
+        uri = qp.getUri()
+        assert uri == req_path
+        entry = self._page_record.getEntry(uri, page_num)
+        if (taxonomy_info is not None or entry is None or
+                entry.used_source_names):
+            cache_key = '%s:%s' % (uri, page_num)
+            app.env.rendered_segments_repository.invalidate(cache_key)
+
+        # Render the page.
+        rendered_page = render_page(render_ctx)
+
+        # Check if this page is a taxonomy page that actually doesn't match
+        # anything.
+        if taxonomy_info is not None:
+            paginator = rendered_page.data.get('pagination')
+            if (paginator and paginator.is_loaded and
+                    len(paginator.items) == 0):
+                taxonomy = taxonomy_info[0]
+                message = ("This URL matched a route for taxonomy '%s' but "
+                           "no pages have been found to have it. This page "
+                           "won't be generated by a bake." % taxonomy.name)
+                raise NotFound(message)
+
+        # Remember stuff for next time.
+        if entry is None:
+            entry = ServeRecordPageEntry(req_path, page_num)
+            self._page_record.addEntry(entry)
+        for p, pinfo in render_ctx.render_passes.items():
+            entry.used_source_names |= pinfo.used_source_names
+
+        # Ok all good.
+        return rendered_page
+
     def _make_wrapped_file_response(self, environ, request, path):
         logger.debug("Serving %s" % path)