comparison 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
comparison
equal deleted inserted replaced
337:49408002798e 338:938be93215cb
2 import shutil 2 import shutil
3 import codecs 3 import codecs
4 import logging 4 import logging
5 import urllib.parse 5 import urllib.parse
6 from piecrust.baking.records import ( 6 from piecrust.baking.records import (
7 FLAG_OVERRIDEN, FLAG_SOURCE_MODIFIED, FLAG_FORCED_BY_SOURCE) 7 BakeRecordPassInfo, BakeRecordPageEntry, BakeRecordSubPageEntry)
8 from piecrust.data.filters import (PaginationFilter, HasFilterClause, 8 from piecrust.data.filters import (
9 PaginationFilter, HasFilterClause,
9 IsFilterClause, AndBooleanClause, 10 IsFilterClause, AndBooleanClause,
10 page_value_accessor) 11 page_value_accessor)
11 from piecrust.rendering import (PageRenderingContext, render_page, 12 from piecrust.rendering import (
13 PageRenderingContext, render_page,
12 PASS_FORMATTING, PASS_RENDERING) 14 PASS_FORMATTING, PASS_RENDERING)
13 from piecrust.sources.base import (PageFactory, 15 from piecrust.sources.base import (
16 PageFactory,
14 REALM_NAMES, REALM_USER, REALM_THEME) 17 REALM_NAMES, REALM_USER, REALM_THEME)
15 from piecrust.uriutil import split_uri 18 from piecrust.uriutil import split_uri
16 19
17 20
18 logger = logging.getLogger(__name__) 21 logger = logging.getLogger(__name__)
58 return os.path.normpath(os.path.join(*bake_path)) 61 return os.path.normpath(os.path.join(*bake_path))
59 62
60 def bake(self, factory, route, record_entry): 63 def bake(self, factory, route, record_entry):
61 bake_taxonomy_info = None 64 bake_taxonomy_info = None
62 route_metadata = dict(factory.metadata) 65 route_metadata = dict(factory.metadata)
66
67 # Add taxonomy metadata for generating the URL if needed.
63 if record_entry.taxonomy_info: 68 if record_entry.taxonomy_info:
64 tax_name, tax_term, tax_source_name = record_entry.taxonomy_info 69 tax_name, tax_term, tax_source_name = record_entry.taxonomy_info
65 taxonomy = self.app.getTaxonomy(tax_name) 70 taxonomy = self.app.getTaxonomy(tax_name)
66 slugified_term = route.slugifyTaxonomyTerm(tax_term) 71 slugified_term = route.slugifyTaxonomyTerm(tax_term)
67 route_metadata[taxonomy.term_name] = slugified_term 72 route_metadata[taxonomy.term_name] = slugified_term
69 74
70 # Generate the URL using the route. 75 # Generate the URL using the route.
71 page = factory.buildPage() 76 page = factory.buildPage()
72 uri = route.getUri(route_metadata, provider=page) 77 uri = route.getUri(route_metadata, provider=page)
73 78
79 # See if this URL has been overriden by a previously baked page.
80 # If that page is from another realm (e.g. a user page vs. a theme
81 # page), we silently skip this page. If they're from the same realm,
82 # we don't allow overriding and raise an error (this is probably
83 # because of a misconfigured configuration that allows for ambiguous
84 # URLs between 2 routes or sources).
74 override = self.record.getOverrideEntry(factory, uri) 85 override = self.record.getOverrideEntry(factory, uri)
75 if override is not None: 86 if override is not None:
76 override_source = self.app.getSource(override.source_name) 87 override_source = self.app.getSource(override.source_name)
77 if override_source.realm == factory.source.realm: 88 if override_source.realm == factory.source.realm:
78 raise BakingError( 89 raise BakingError(
81 override.source_name, 92 override.source_name,
82 override.rel_path)) 93 override.rel_path))
83 logger.debug("'%s' [%s] is overriden by '%s:%s'. Skipping" % 94 logger.debug("'%s' [%s] is overriden by '%s:%s'. Skipping" %
84 (factory.ref_spec, uri, override.source_name, 95 (factory.ref_spec, uri, override.source_name,
85 override.rel_path)) 96 override.rel_path))
86 record_entry.flags |= FLAG_OVERRIDEN 97 record_entry.flags |= BakeRecordPageEntry.FLAG_OVERRIDEN
87 return 98 return
88 99
100 # Setup the record entry.
101 record_entry.config = copy_public_page_config(page.config)
102
103 # Start baking the sub-pages.
89 cur_sub = 1 104 cur_sub = 1
90 has_more_subs = True 105 has_more_subs = True
91 force_this = self.force 106 force_this = self.force
92 invalidate_formatting = False 107 invalidate_formatting = False
93 record_entry.config = copy_public_page_config(page.config)
94 prev_record_entry = self.record.getPreviousEntry( 108 prev_record_entry = self.record.getPreviousEntry(
95 factory.source.name, factory.rel_path, 109 factory.source.name, factory.rel_path,
96 record_entry.taxonomy_info) 110 record_entry.taxonomy_info)
97 111
98 logger.debug("Baking '%s'..." % uri) 112 logger.debug("Baking '%s'..." % uri)
99 113
100 # If the current page is known to use pages from other sources,
101 # see if any of those got baked, or are going to be baked for some
102 # reason. If so, we need to bake this one too.
103 # (this happens for instance with the main page of a blog).
104 if prev_record_entry and prev_record_entry.was_baked_successfully:
105 invalidated_render_passes = set()
106 used_src_names = list(prev_record_entry.used_source_names)
107 for src_name, rdr_pass in used_src_names:
108 entries = self.record.getCurrentEntries(src_name)
109 for e in entries:
110 if e.was_baked or e.flags & FLAG_SOURCE_MODIFIED:
111 invalidated_render_passes.add(rdr_pass)
112 break
113 if len(invalidated_render_passes) > 0:
114 logger.debug("'%s' is known to use sources %s, at least one "
115 "of which got baked. Will force bake this page. "
116 % (uri, used_src_names))
117 record_entry.flags |= FLAG_FORCED_BY_SOURCE
118 force_this = True
119
120 if PASS_FORMATTING in invalidated_render_passes:
121 logger.debug("Will invalidate cached formatting for '%s' "
122 "since sources were using during that pass."
123 % uri)
124 invalidate_formatting = True
125
126 while has_more_subs: 114 while has_more_subs:
115 # Get the URL and path for this sub-page.
127 sub_uri = route.getUri(route_metadata, sub_num=cur_sub, 116 sub_uri = route.getUri(route_metadata, sub_num=cur_sub,
128 provider=page) 117 provider=page)
129 out_path = self.getOutputPath(sub_uri) 118 out_path = self.getOutputPath(sub_uri)
119
120 # Create the sub-entry for the bake record.
121 record_sub_entry = BakeRecordSubPageEntry(sub_uri, out_path)
122 record_entry.subs.append(record_sub_entry)
123
124 # Find a corresponding sub-entry in the previous bake record.
125 prev_record_sub_entry = None
126 if prev_record_entry:
127 try:
128 prev_record_sub_entry = prev_record_entry.getSub(cur_sub)
129 except IndexError:
130 pass
131
132 # Figure out what to do with this page.
133 if (prev_record_sub_entry and
134 (prev_record_sub_entry.was_baked_successfully or
135 prev_record_sub_entry.was_clean)):
136 # If the current page is known to use pages from other sources,
137 # see if any of those got baked, or are going to be baked for
138 # some reason. If so, we need to bake this one too.
139 # (this happens for instance with the main page of a blog).
140 dirty_src_names, invalidated_render_passes = (
141 self._getDirtySourceNamesAndRenderPasses(
142 prev_record_sub_entry))
143 if len(invalidated_render_passes) > 0:
144 logger.debug(
145 "'%s' is known to use sources %s, which have "
146 "items that got (re)baked. Will force bake this "
147 "page. " % (uri, dirty_src_names))
148 record_sub_entry.flags |= \
149 BakeRecordSubPageEntry.FLAG_FORCED_BY_SOURCE
150 force_this = True
151
152 if PASS_FORMATTING in invalidated_render_passes:
153 logger.debug(
154 "Will invalidate cached formatting for '%s' "
155 "since sources were using during that pass."
156 % uri)
157 invalidate_formatting = True
158 elif (prev_record_sub_entry and
159 prev_record_sub_entry.errors):
160 # Previous bake failed. We'll have to bake it again.
161 logger.debug(
162 "Previous record entry indicates baking failed for "
163 "'%s'. Will bake it again." % uri)
164 record_sub_entry.flags |= \
165 BakeRecordSubPageEntry.FLAG_FORCED_BY_PREVIOUS_ERRORS
166 force_this = True
167 elif not prev_record_sub_entry:
168 # No previous record. We'll have to bake it.
169 logger.debug("No previous record entry found for '%s'. Will "
170 "force bake it." % uri)
171 record_sub_entry.flags |= \
172 BakeRecordSubPageEntry.FLAG_FORCED_BY_NO_PREVIOUS
173 force_this = True
130 174
131 # Check for up-to-date outputs. 175 # Check for up-to-date outputs.
132 do_bake = True 176 do_bake = True
133 if not force_this: 177 if not force_this:
134 try: 178 try:
141 pass 185 pass
142 186
143 # If this page didn't bake because it's already up-to-date. 187 # If this page didn't bake because it's already up-to-date.
144 # Keep trying for as many subs as we know this page has. 188 # Keep trying for as many subs as we know this page has.
145 if not do_bake: 189 if not do_bake:
146 if (prev_record_entry is not None and 190 prev_record_sub_entry.collapseRenderPasses(record_sub_entry)
147 prev_record_entry.num_subs < cur_sub): 191 record_sub_entry.flags = BakeRecordSubPageEntry.FLAG_NONE
148 logger.debug("") 192
193 if prev_record_entry.num_subs >= cur_sub + 1:
149 cur_sub += 1 194 cur_sub += 1
150 has_more_subs = True 195 has_more_subs = True
151 logger.debug(" %s is up to date, skipping to next " 196 logger.debug(" %s is up to date, skipping to next "
152 "sub-page." % out_path) 197 "sub-page." % out_path)
153 record_entry.clean_uris.append(sub_uri)
154 record_entry.clean_out_paths.append(out_path)
155 continue 198 continue
156 199
157 # We don't know how many subs to expect... just skip.
158 logger.debug(" %s is up to date, skipping bake." % out_path) 200 logger.debug(" %s is up to date, skipping bake." % out_path)
159 break 201 break
160 202
161 # All good, proceed. 203 # All good, proceed.
162 try: 204 try:
163 if invalidate_formatting: 205 if invalidate_formatting:
164 cache_key = sub_uri 206 cache_key = sub_uri
165 self.app.env.rendered_segments_repository.invalidate( 207 self.app.env.rendered_segments_repository.invalidate(
166 cache_key) 208 cache_key)
209 record_sub_entry.flags |= \
210 BakeRecordSubPageEntry.FLAG_FORMATTING_INVALIDATED
167 211
168 logger.debug(" p%d -> %s" % (cur_sub, out_path)) 212 logger.debug(" p%d -> %s" % (cur_sub, out_path))
169 ctx, rp = self._bakeSingle(page, sub_uri, cur_sub, out_path, 213 ctx, rp = self._bakeSingle(page, sub_uri, cur_sub, out_path,
170 bake_taxonomy_info) 214 bake_taxonomy_info)
171 except Exception as ex: 215 except Exception as ex:
172 if self.app.debug: 216 if self.app.debug:
173 logger.exception(ex) 217 logger.exception(ex)
174 page_rel_path = os.path.relpath(page.path, self.app.root_dir) 218 page_rel_path = os.path.relpath(page.path, self.app.root_dir)
175 raise BakingError("%s: error baking '%s'." % 219 raise BakingError("%s: error baking '%s'." %
176 (page_rel_path, uri)) from ex 220 (page_rel_path, uri)) from ex
221
222 # Record what we did.
223 record_sub_entry.flags |= BakeRecordSubPageEntry.FLAG_BAKED
224 self.record.dirty_source_names.add(record_entry.source_name)
225 for p, pinfo in ctx.render_passes.items():
226 brpi = BakeRecordPassInfo()
227 brpi.used_source_names = set(pinfo.used_source_names)
228 brpi.used_taxonomy_terms = set(pinfo.used_taxonomy_terms)
229 record_sub_entry.render_passes[p] = brpi
230 if prev_record_sub_entry:
231 prev_record_sub_entry.collapseRenderPasses(record_sub_entry)
177 232
178 # Copy page assets. 233 # Copy page assets.
179 if (cur_sub == 1 and self.copy_assets and 234 if (cur_sub == 1 and self.copy_assets and
180 ctx.used_assets is not None): 235 ctx.used_assets is not None):
181 if self.pretty_urls: 236 if self.pretty_urls:
188 243
189 logger.debug("Copying page assets to: %s" % out_assets_dir) 244 logger.debug("Copying page assets to: %s" % out_assets_dir)
190 if not os.path.isdir(out_assets_dir): 245 if not os.path.isdir(out_assets_dir):
191 os.makedirs(out_assets_dir, 0o755) 246 os.makedirs(out_assets_dir, 0o755)
192 for ap in ctx.used_assets: 247 for ap in ctx.used_assets:
193 dest_ap = os.path.join(out_assets_dir, os.path.basename(ap)) 248 dest_ap = os.path.join(out_assets_dir,
249 os.path.basename(ap))
194 logger.debug(" %s -> %s" % (ap, dest_ap)) 250 logger.debug(" %s -> %s" % (ap, dest_ap))
195 shutil.copy(ap, dest_ap) 251 shutil.copy(ap, dest_ap)
196 252 record_entry.assets.append(ap)
197 # Record what we did and figure out if we have more work. 253
198 record_entry.out_uris.append(sub_uri) 254 # Figure out if we have more work.
199 record_entry.out_paths.append(out_path)
200 record_entry.used_source_names |= ctx.used_source_names
201 record_entry.used_taxonomy_terms |= ctx.used_taxonomy_terms
202
203 has_more_subs = False 255 has_more_subs = False
204 if ctx.used_pagination is not None: 256 if ctx.used_pagination is not None:
205 if cur_sub == 1:
206 record_entry.used_pagination_item_count = \
207 ctx.used_pagination.total_item_count
208 if ctx.used_pagination.has_more: 257 if ctx.used_pagination.has_more:
209 cur_sub += 1 258 cur_sub += 1
210 has_more_subs = True 259 has_more_subs = True
211 260
212 def _bakeSingle(self, page, sub_uri, num, out_path, 261 def _bakeSingle(self, page, sub_uri, num, out_path,
225 with codecs.open(out_path, 'w', 'utf8') as fp: 274 with codecs.open(out_path, 'w', 'utf8') as fp:
226 fp.write(rp.content) 275 fp.write(rp.content)
227 276
228 return ctx, rp 277 return ctx, rp
229 278
279 def _getDirtySourceNamesAndRenderPasses(self, record_sub_entry):
280 dirty_src_names = set()
281 invalidated_render_passes = set()
282 for p, pinfo in record_sub_entry.render_passes.items():
283 for src_name in pinfo.used_source_names:
284 is_dirty = (src_name in self.record.dirty_source_names)
285 if is_dirty:
286 invalidated_render_passes.add(p)
287 dirty_src_names.add(src_name)
288 break
289 return dirty_src_names, invalidated_render_passes
290