comparison piecrust/baking/single.py @ 150:91dcbb5fe1e8

Split baking code in smaller files.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 30 Nov 2014 21:46:42 -0800
parents
children 55087da9a72e
comparison
equal deleted inserted replaced
149:ea4a17831242 150:91dcbb5fe1e8
1 import os.path
2 import shutil
3 import codecs
4 import logging
5 import urllib.error
6 import urllib.parse
7 import urllib.request
8 from piecrust.baking.records import FLAG_OVERRIDEN, FLAG_SOURCE_MODIFIED
9 from piecrust.data.filters import (PaginationFilter, HasFilterClause,
10 IsFilterClause, AndBooleanClause)
11 from piecrust.rendering import (PageRenderingContext, render_page,
12 PASS_FORMATTING, PASS_RENDERING)
13 from piecrust.sources.base import (PageFactory,
14 REALM_NAMES, REALM_USER, REALM_THEME)
15
16
17 logger = logging.getLogger(__name__)
18
19
20 class BakingError(Exception):
21 pass
22
23
24 class PageBaker(object):
25 def __init__(self, app, out_dir, force=False, record=None,
26 copy_assets=True):
27 self.app = app
28 self.out_dir = out_dir
29 self.force = force
30 self.record = record
31 self.copy_assets = copy_assets
32 self.site_root = app.config.get('site/root')
33 self.pretty_urls = app.config.get('site/pretty_urls')
34 self.pagination_suffix = app.config.get('site/pagination_suffix')
35
36 def getOutputUri(self, uri, num):
37 suffix = self.pagination_suffix.replace('%num%', str(num))
38 if self.pretty_urls:
39 # Output will be:
40 # - `uri/name`
41 # - `uri/name/2`
42 # - `uri/name.ext`
43 # - `uri/name.ext/2`
44 if num <= 1:
45 return uri
46 return uri + suffix
47 else:
48 # Output will be:
49 # - `uri/name.html`
50 # - `uri/name/2.html`
51 # - `uri/name.ext`
52 # - `uri/name/2.ext`
53 if uri == '/':
54 if num <= 1:
55 return '/'
56 return '/' + suffix.lstrip('/')
57 else:
58 if num <= 1:
59 return uri
60 #TODO: watch out for tags with dots in them.
61 base_uri, ext = os.path.splitext(uri)
62 return base_uri + suffix + ext
63
64 def getOutputPath(self, uri):
65 bake_path = [self.out_dir]
66 decoded_uri = urllib.parse.unquote(uri.lstrip('/'))
67 if self.pretty_urls:
68 bake_path.append(decoded_uri)
69 bake_path.append('index.html')
70 else:
71 name, ext = os.path.splitext(decoded_uri)
72 if decoded_uri == '':
73 bake_path.append('index.html')
74 elif ext:
75 bake_path.append(decoded_uri)
76 else:
77 bake_path.append(decoded_uri + '.html')
78
79 return os.path.normpath(os.path.join(*bake_path))
80
81 def bake(self, factory, route, record_entry,
82 taxonomy_name=None, taxonomy_term=None):
83 custom_data = None
84 pagination_filter = None
85 route_metadata = dict(factory.metadata)
86 if taxonomy_name and taxonomy_term:
87 # Must bake a taxonomy listing page... we'll have to add a
88 # pagination filter for only get matching posts, and the output
89 # URL will be a bit different.
90 tax = self.app.getTaxonomy(taxonomy_name)
91 pagination_filter = PaginationFilter()
92 if tax.is_multiple:
93 if isinstance(taxonomy_term, tuple):
94 abc = AndBooleanClause()
95 for t in taxonomy_term:
96 abc.addClause(HasFilterClause(taxonomy_name, t))
97 pagination_filter.addClause(abc)
98 slugified_term = '/'.join(taxonomy_term)
99 else:
100 pagination_filter.addClause(
101 HasFilterClause(taxonomy_name, taxonomy_term))
102 slugified_term = taxonomy_term
103 else:
104 pagination_filter.addClause(
105 IsFilterClause(taxonomy_name, taxonomy_term))
106 slugified_term = taxonomy_term
107 custom_data = {tax.term_name: taxonomy_term}
108 route_metadata.update({tax.term_name: slugified_term})
109
110 # Generate the URL using the route.
111 page = factory.buildPage()
112 uri = route.getUri(route_metadata, page)
113
114 override = self.record.getOverrideEntry(factory, uri)
115 if override is not None:
116 override_source = self.app.getSource(override.source_name)
117 if override_source.realm == factory.source.realm:
118 raise BakingError(
119 "Page '%s' maps to URL '%s' but is overriden by page"
120 "'%s:%s'." % (factory.ref_spec, uri,
121 override.source_name, override.rel_path))
122 logger.debug("'%s' [%s] is overriden by '%s:%s'. Skipping" %
123 (factory.ref_spec, uri, override.source_name,
124 override.rel_path))
125 record_entry.flags |= FLAG_OVERRIDEN
126 return
127
128 cur_sub = 1
129 has_more_subs = True
130 force_this = self.force
131 invalidate_formatting = False
132 record_entry.config = page.config.get().copy()
133 prev_record_entry = self.record.getPreviousEntry(
134 factory.source.name, factory.rel_path,
135 taxonomy_name, taxonomy_term)
136
137 logger.debug("Baking '%s'..." % uri)
138
139 # If the current page is known to use pages from other sources,
140 # see if any of those got baked, or are going to be baked for some
141 # reason. If so, we need to bake this one too.
142 # (this happens for instance with the main page of a blog).
143 if prev_record_entry and prev_record_entry.was_baked_successfully:
144 invalidated_render_passes = set()
145 used_src_names = list(prev_record_entry.used_source_names)
146 for src_name, rdr_pass in used_src_names:
147 entries = self.record.getCurrentEntries(src_name)
148 for e in entries:
149 if e.was_baked or e.flags & FLAG_SOURCE_MODIFIED:
150 invalidated_render_passes.add(rdr_pass)
151 break
152 if len(invalidated_render_passes) > 0:
153 logger.debug("'%s' is known to use sources %s, at least one "
154 "of which got baked. Will force bake this page. "
155 % (uri, used_src_names))
156 force_this = True
157 if PASS_FORMATTING in invalidated_render_passes:
158 logger.debug("Will invalidate cached formatting for '%s' "
159 "since sources were using during that pass."
160 % uri)
161 invalidate_formatting = True
162
163 while has_more_subs:
164 sub_uri = self.getOutputUri(uri, cur_sub)
165 out_path = self.getOutputPath(sub_uri)
166
167 # Check for up-to-date outputs.
168 do_bake = True
169 if not force_this:
170 try:
171 in_path_time = record_entry.path_mtime
172 out_path_time = os.path.getmtime(out_path)
173 if out_path_time > in_path_time:
174 do_bake = False
175 except OSError:
176 # File doesn't exist, we'll need to bake.
177 pass
178
179 # If this page didn't bake because it's already up-to-date.
180 # Keep trying for as many subs as we know this page has.
181 if not do_bake:
182 if (prev_record_entry is not None and
183 prev_record_entry.num_subs < cur_sub):
184 logger.debug("")
185 cur_sub += 1
186 has_more_subs = True
187 logger.debug(" %s is up to date, skipping to next "
188 "sub-page." % out_path)
189 continue
190
191 # We don't know how many subs to expect... just skip.
192 logger.debug(" %s is up to date, skipping bake." % out_path)
193 break
194
195 # All good, proceed.
196 try:
197 if invalidate_formatting:
198 cache_key = '%s:%s' % (uri, cur_sub)
199 self.app.env.rendered_segments_repository.invalidate(
200 cache_key)
201
202 logger.debug(" p%d -> %s" % (cur_sub, out_path))
203 ctx, rp = self._bakeSingle(page, sub_uri, cur_sub, out_path,
204 pagination_filter, custom_data)
205 except Exception as ex:
206 if self.app.debug:
207 logger.exception(ex)
208 page_rel_path = os.path.relpath(page.path, self.app.root_dir)
209 raise BakingError("%s: error baking '%s'." %
210 (page_rel_path, uri)) from ex
211
212 # Copy page assets.
213 if (cur_sub == 1 and self.copy_assets and
214 ctx.used_assets is not None):
215 if self.pretty_urls:
216 out_assets_dir = os.path.dirname(out_path)
217 else:
218 out_assets_dir, out_name = os.path.split(out_path)
219 if sub_uri != self.site_root:
220 out_name_noext, _ = os.path.splitext(out_name)
221 out_assets_dir += out_name_noext
222
223 logger.debug("Copying page assets to: %s" % out_assets_dir)
224 if not os.path.isdir(out_assets_dir):
225 os.makedirs(out_assets_dir, 0o755)
226 for ap in ctx.used_assets:
227 dest_ap = os.path.join(out_assets_dir, os.path.basename(ap))
228 logger.debug(" %s -> %s" % (ap, dest_ap))
229 shutil.copy(ap, dest_ap)
230
231 # Record what we did and figure out if we have more work.
232 record_entry.out_uris.append(sub_uri)
233 record_entry.out_paths.append(out_path)
234 record_entry.used_source_names |= ctx.used_source_names
235 record_entry.used_taxonomy_terms |= ctx.used_taxonomy_terms
236
237 has_more_subs = False
238 if (ctx.used_pagination is not None and
239 ctx.used_pagination.has_more):
240 cur_sub += 1
241 has_more_subs = True
242
243 def _bakeSingle(self, page, sub_uri, num, out_path,
244 pagination_filter=None, custom_data=None):
245 ctx = PageRenderingContext(page, sub_uri)
246 ctx.page_num = num
247 if pagination_filter:
248 ctx.pagination_filter = pagination_filter
249 if custom_data:
250 ctx.custom_data = custom_data
251
252 rp = render_page(ctx)
253
254 out_dir = os.path.dirname(out_path)
255 if not os.path.isdir(out_dir):
256 os.makedirs(out_dir, 0o755)
257
258 with codecs.open(out_path, 'w', 'utf8') as fp:
259 fp.write(rp.content)
260
261 return ctx, rp
262