diff piecrust/baking/single.py @ 338:938be93215cb

bake: Improve render context and bake record, fix incremental bake bugs. * Used sources and taxonomies are now stored on a per-render-pass basis. This fixes bugs where sources/taxonomies were used for one pass, but that pass is skipped on a later bake because its result is cached. * Bake records are now created for all pages even when they're not baked. Record collapsing is gone except for taxonomy index pages. * Bake records now also have sub-entries in order to store information about each sub-page, since some sub-pages could use sources/taxonomies differently than others, or be missing from the output. This lets PieCrust handle clean/dirty states on a sub-page level.
author Ludovic Chabant <ludovic@chabant.com>
date Mon, 06 Apr 2015 19:59:54 -0700
parents b034f6f15e22
children b8ff1780b491
line wrap: on
line diff
--- a/piecrust/baking/single.py	Sat Apr 04 07:55:49 2015 -0700
+++ b/piecrust/baking/single.py	Mon Apr 06 19:59:54 2015 -0700
@@ -4,13 +4,16 @@
 import logging
 import urllib.parse
 from piecrust.baking.records import (
-        FLAG_OVERRIDEN, FLAG_SOURCE_MODIFIED, FLAG_FORCED_BY_SOURCE)
-from piecrust.data.filters import (PaginationFilter, HasFilterClause,
+        BakeRecordPassInfo, BakeRecordPageEntry, BakeRecordSubPageEntry)
+from piecrust.data.filters import (
+        PaginationFilter, HasFilterClause,
         IsFilterClause, AndBooleanClause,
         page_value_accessor)
-from piecrust.rendering import (PageRenderingContext, render_page,
+from piecrust.rendering import (
+        PageRenderingContext, render_page,
         PASS_FORMATTING, PASS_RENDERING)
-from piecrust.sources.base import (PageFactory,
+from piecrust.sources.base import (
+        PageFactory,
         REALM_NAMES, REALM_USER, REALM_THEME)
 from piecrust.uriutil import split_uri
 
@@ -60,6 +63,8 @@
     def bake(self, factory, route, record_entry):
         bake_taxonomy_info = None
         route_metadata = dict(factory.metadata)
+
+        # Add taxonomy metadata for generating the URL if needed.
         if record_entry.taxonomy_info:
             tax_name, tax_term, tax_source_name = record_entry.taxonomy_info
             taxonomy = self.app.getTaxonomy(tax_name)
@@ -71,6 +76,12 @@
         page = factory.buildPage()
         uri = route.getUri(route_metadata, provider=page)
 
+        # See if this URL has been overriden by a previously baked page.
+        # If that page is from another realm (e.g. a user page vs. a theme
+        # page), we silently skip this page. If they're from the same realm,
+        # we don't allow overriding and raise an error (this is probably
+        # because of a misconfigured configuration that allows for ambiguous
+        # URLs between 2 routes or sources).
         override = self.record.getOverrideEntry(factory, uri)
         if override is not None:
             override_source = self.app.getSource(override.source_name)
@@ -83,51 +94,84 @@
             logger.debug("'%s' [%s] is overriden by '%s:%s'. Skipping" %
                          (factory.ref_spec, uri, override.source_name,
                           override.rel_path))
-            record_entry.flags |= FLAG_OVERRIDEN
+            record_entry.flags |= BakeRecordPageEntry.FLAG_OVERRIDEN
             return
 
+        # Setup the record entry.
+        record_entry.config = copy_public_page_config(page.config)
+
+        # Start baking the sub-pages.
         cur_sub = 1
         has_more_subs = True
         force_this = self.force
         invalidate_formatting = False
-        record_entry.config = copy_public_page_config(page.config)
         prev_record_entry = self.record.getPreviousEntry(
                 factory.source.name, factory.rel_path,
                 record_entry.taxonomy_info)
 
         logger.debug("Baking '%s'..." % uri)
 
-        # If the current page is known to use pages from other sources,
-        # see if any of those got baked, or are going to be baked for some
-        # reason. If so, we need to bake this one too.
-        # (this happens for instance with the main page of a blog).
-        if prev_record_entry and prev_record_entry.was_baked_successfully:
-            invalidated_render_passes = set()
-            used_src_names = list(prev_record_entry.used_source_names)
-            for src_name, rdr_pass in used_src_names:
-                entries = self.record.getCurrentEntries(src_name)
-                for e in entries:
-                    if e.was_baked or e.flags & FLAG_SOURCE_MODIFIED:
-                        invalidated_render_passes.add(rdr_pass)
-                        break
-            if len(invalidated_render_passes) > 0:
-                logger.debug("'%s' is known to use sources %s, at least one "
-                             "of which got baked. Will force bake this page. "
-                             % (uri, used_src_names))
-                record_entry.flags |= FLAG_FORCED_BY_SOURCE
-                force_this = True
-
-                if PASS_FORMATTING in invalidated_render_passes:
-                    logger.debug("Will invalidate cached formatting for '%s' "
-                                 "since sources were using during that pass."
-                                 % uri)
-                    invalidate_formatting = True
-
         while has_more_subs:
+            # Get the URL and path for this sub-page.
             sub_uri = route.getUri(route_metadata, sub_num=cur_sub,
                                    provider=page)
             out_path = self.getOutputPath(sub_uri)
 
+            # Create the sub-entry for the bake record.
+            record_sub_entry = BakeRecordSubPageEntry(sub_uri, out_path)
+            record_entry.subs.append(record_sub_entry)
+
+            # Find a corresponding sub-entry in the previous bake record.
+            prev_record_sub_entry = None
+            if prev_record_entry:
+                try:
+                    prev_record_sub_entry = prev_record_entry.getSub(cur_sub)
+                except IndexError:
+                    pass
+
+            # Figure out what to do with this page.
+            if (prev_record_sub_entry and
+                    (prev_record_sub_entry.was_baked_successfully or
+                        prev_record_sub_entry.was_clean)):
+                # If the current page is known to use pages from other sources,
+                # see if any of those got baked, or are going to be baked for
+                # some reason. If so, we need to bake this one too.
+                # (this happens for instance with the main page of a blog).
+                dirty_src_names, invalidated_render_passes = (
+                        self._getDirtySourceNamesAndRenderPasses(
+                            prev_record_sub_entry))
+                if len(invalidated_render_passes) > 0:
+                    logger.debug(
+                            "'%s' is known to use sources %s, which have "
+                            "items that got (re)baked. Will force bake this "
+                            "page. " % (uri, dirty_src_names))
+                    record_sub_entry.flags |= \
+                        BakeRecordSubPageEntry.FLAG_FORCED_BY_SOURCE
+                    force_this = True
+
+                    if PASS_FORMATTING in invalidated_render_passes:
+                        logger.debug(
+                                "Will invalidate cached formatting for '%s' "
+                                "since sources were using during that pass."
+                                % uri)
+                        invalidate_formatting = True
+            elif (prev_record_sub_entry and
+                    prev_record_sub_entry.errors):
+                # Previous bake failed. We'll have to bake it again.
+                logger.debug(
+                        "Previous record entry indicates baking failed for "
+                        "'%s'. Will bake it again." % uri)
+                record_sub_entry.flags |= \
+                    BakeRecordSubPageEntry.FLAG_FORCED_BY_PREVIOUS_ERRORS
+                force_this = True
+            elif not prev_record_sub_entry:
+                # No previous record. We'll have to bake it.
+                logger.debug("No previous record entry found for '%s'. Will "
+                             "force bake it." % uri)
+                record_sub_entry.flags |= \
+                    BakeRecordSubPageEntry.FLAG_FORCED_BY_NO_PREVIOUS
+                force_this = True
+
             # Check for up-to-date outputs.
             do_bake = True
             if not force_this:
@@ -143,18 +187,16 @@
             # If this page didn't bake because it's already up-to-date.
             # Keep trying for as many subs as we know this page has.
             if not do_bake:
-                if (prev_record_entry is not None and
-                        prev_record_entry.num_subs < cur_sub):
-                    logger.debug("")
+                prev_record_sub_entry.collapseRenderPasses(record_sub_entry)
+                record_sub_entry.flags = BakeRecordSubPageEntry.FLAG_NONE
+
+                if prev_record_entry.num_subs >= cur_sub + 1:
                     cur_sub += 1
                     has_more_subs = True
                     logger.debug("  %s is up to date, skipping to next "
                                  "sub-page." % out_path)
-                    record_entry.clean_uris.append(sub_uri)
-                    record_entry.clean_out_paths.append(out_path)
                     continue
 
-                # We don't know how many subs to expect... just skip.
                 logger.debug("  %s is up to date, skipping bake." % out_path)
                 break
 
@@ -164,6 +206,8 @@
                     cache_key = sub_uri
                     self.app.env.rendered_segments_repository.invalidate(
                             cache_key)
+                    record_sub_entry.flags |= \
+                        BakeRecordSubPageEntry.FLAG_FORMATTING_INVALIDATED
 
                 logger.debug("  p%d -> %s" % (cur_sub, out_path))
                 ctx, rp = self._bakeSingle(page, sub_uri, cur_sub, out_path,
@@ -175,6 +219,17 @@
                 raise BakingError("%s: error baking '%s'." %
                                   (page_rel_path, uri)) from ex
 
+            # Record what we did.
+            record_sub_entry.flags |= BakeRecordSubPageEntry.FLAG_BAKED
+            self.record.dirty_source_names.add(record_entry.source_name)
+            for p, pinfo in ctx.render_passes.items():
+                brpi = BakeRecordPassInfo()
+                brpi.used_source_names = set(pinfo.used_source_names)
+                brpi.used_taxonomy_terms = set(pinfo.used_taxonomy_terms)
+                record_sub_entry.render_passes[p] = brpi
+            if prev_record_sub_entry:
+                prev_record_sub_entry.collapseRenderPasses(record_sub_entry)
+
             # Copy page assets.
             if (cur_sub == 1 and self.copy_assets and
                     ctx.used_assets is not None):
@@ -190,21 +245,15 @@
                 if not os.path.isdir(out_assets_dir):
                     os.makedirs(out_assets_dir, 0o755)
                 for ap in ctx.used_assets:
-                    dest_ap = os.path.join(out_assets_dir, os.path.basename(ap))
+                    dest_ap = os.path.join(out_assets_dir,
+                                           os.path.basename(ap))
                     logger.debug("  %s -> %s" % (ap, dest_ap))
                     shutil.copy(ap, dest_ap)
+                    record_entry.assets.append(ap)
 
-            # Record what we did and figure out if we have more work.
-            record_entry.out_uris.append(sub_uri)
-            record_entry.out_paths.append(out_path)
-            record_entry.used_source_names |= ctx.used_source_names
-            record_entry.used_taxonomy_terms |= ctx.used_taxonomy_terms
-
+            # Figure out if we have more work.
             has_more_subs = False
             if ctx.used_pagination is not None:
-                if cur_sub == 1:
-                    record_entry.used_pagination_item_count = \
-                            ctx.used_pagination.total_item_count
                 if ctx.used_pagination.has_more:
                     cur_sub += 1
                     has_more_subs = True
@@ -227,3 +276,15 @@
 
         return ctx, rp
 
+    def _getDirtySourceNamesAndRenderPasses(self, record_sub_entry):
+        dirty_src_names = set()
+        invalidated_render_passes = set()
+        for p, pinfo in record_sub_entry.render_passes.items():
+            for src_name in pinfo.used_source_names:
+                is_dirty = (src_name in self.record.dirty_source_names)
+                if is_dirty:
+                    invalidated_render_passes.add(p)
+                    dirty_src_names.add(src_name)
+                    break
+        return dirty_src_names, invalidated_render_passes
+