comparison piecrust/baking/records.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 2cd2b5d07129
comparison
equal deleted inserted replaced
337:49408002798e 338:938be93215cb
1 import copy
1 import os.path 2 import os.path
2 import logging 3 import logging
3 from piecrust.records import Record, TransitionalRecord 4 from piecrust.records import Record, TransitionalRecord
4 5
5 6
17 key += taxonomy_term 18 key += taxonomy_term
18 return key 19 return key
19 20
20 21
21 class BakeRecord(Record): 22 class BakeRecord(Record):
22 RECORD_VERSION = 11 23 RECORD_VERSION = 12
23 24
24 def __init__(self): 25 def __init__(self):
25 super(BakeRecord, self).__init__() 26 super(BakeRecord, self).__init__()
26 self.out_dir = None 27 self.out_dir = None
27 self.bake_time = None 28 self.bake_time = None
28 self.success = True 29 self.success = True
29 30
30 31
31 FLAG_NONE = 0 32 class BakeRecordPassInfo(object):
32 FLAG_SOURCE_MODIFIED = 2**0 33 def __init__(self):
33 FLAG_OVERRIDEN = 2**1 34 self.used_source_names = set()
34 FLAG_FORCED_BY_SOURCE = 2**2 35 self.used_taxonomy_terms = set()
36
37
38 class BakeRecordSubPageEntry(object):
39 FLAG_NONE = 0
40 FLAG_BAKED = 2**0
41 FLAG_FORCED_BY_SOURCE = 2**1
42 FLAG_FORCED_BY_NO_PREVIOUS = 2**2
43 FLAG_FORCED_BY_PREVIOUS_ERRORS = 2**3
44 FLAG_FORMATTING_INVALIDATED = 2**4
45
46 def __init__(self, out_uri, out_path):
47 self.out_uri = out_uri
48 self.out_path = out_path
49 self.flags = self.FLAG_NONE
50 self.errors = []
51 self.render_passes = {}
52
53 @property
54 def was_clean(self):
55 return (self.flags & self.FLAG_BAKED) == 0 and len(self.errors) == 0
56
57 @property
58 def was_baked(self):
59 return (self.flags & self.FLAG_BAKED) != 0
60
61 @property
62 def was_baked_successfully(self):
63 return self.was_baked and len(self.errors) == 0
64
65 def collapseRenderPasses(self, other):
66 for p, pinfo in self.render_passes.items():
67 if p not in other.render_passes:
68 other.render_passes[p] = copy.deepcopy(pinfo)
35 69
36 70
37 class BakeRecordPageEntry(object): 71 class BakeRecordPageEntry(object):
38 """ An entry in the bake record. 72 """ An entry in the bake record.
39 73
40 The `taxonomy_info` attribute should be a tuple of the form: 74 The `taxonomy_info` attribute should be a tuple of the form:
41 (taxonomy name, term, source name) 75 (taxonomy name, term, source name)
42 """ 76 """
77 FLAG_NONE = 0
78 FLAG_NEW = 2**0
79 FLAG_SOURCE_MODIFIED = 2**1
80 FLAG_OVERRIDEN = 2**2
81
43 def __init__(self, source_name, rel_path, path, taxonomy_info=None): 82 def __init__(self, source_name, rel_path, path, taxonomy_info=None):
44 self.source_name = source_name 83 self.source_name = source_name
45 self.rel_path = rel_path 84 self.rel_path = rel_path
46 self.path = path 85 self.path = path
47 self.taxonomy_info = taxonomy_info 86 self.taxonomy_info = taxonomy_info
48 87 self.flags = self.FLAG_NONE
49 self.flags = FLAG_NONE
50 self.config = None 88 self.config = None
51 self.errors = [] 89 self.subs = []
52 self.out_uris = [] 90 self.assets = []
53 self.out_paths = []
54 self.clean_uris = []
55 self.clean_out_paths = []
56 self.used_source_names = set()
57 self.used_taxonomy_terms = set()
58 self.used_pagination_item_count = 0
59 91
60 @property 92 @property
61 def path_mtime(self): 93 def path_mtime(self):
62 return os.path.getmtime(self.path) 94 return os.path.getmtime(self.path)
63 95
64 @property 96 @property
65 def was_baked(self): 97 def was_overriden(self):
66 return len(self.out_paths) > 0 or len(self.errors) > 0 98 return (self.flags & self.FLAG_OVERRIDEN) != 0
67
68 @property
69 def was_baked_successfully(self):
70 return len(self.out_paths) > 0 and len(self.errors) == 0
71 99
72 @property 100 @property
73 def num_subs(self): 101 def num_subs(self):
74 return len(self.out_paths) 102 return len(self.subs)
103
104 @property
105 def was_any_sub_baked(self):
106 for o in self.subs:
107 if o.was_baked:
108 return True
109 return False
110
111 def getSub(self, sub_index):
112 return self.subs[sub_index - 1]
113
114 def getAllErrors(self):
115 for o in self.subs:
116 yield from o.errors
117
118 def getAllUsedSourceNames(self):
119 res = set()
120 for o in self.subs:
121 for p, pinfo in o.render_passes.items():
122 res |= pinfo.used_source_names
123 return res
124
125 def getAllUsedTaxonomyTerms(self):
126 res = set()
127 for o in self.subs:
128 for p, pinfo in o.render_passes.items():
129 res |= pinfo.used_taxonomy_terms
130 return res
75 131
76 132
77 class TransitionalBakeRecord(TransitionalRecord): 133 class TransitionalBakeRecord(TransitionalRecord):
78 def __init__(self, previous_path=None): 134 def __init__(self, previous_path=None):
79 super(TransitionalBakeRecord, self).__init__(BakeRecord, 135 super(TransitionalBakeRecord, self).__init__(BakeRecord,
80 previous_path) 136 previous_path)
137 self.dirty_source_names = set()
81 138
82 def addEntry(self, entry): 139 def addEntry(self, entry):
83 if (self.previous.bake_time and 140 if (self.previous.bake_time and
84 entry.path_mtime >= self.previous.bake_time): 141 entry.path_mtime >= self.previous.bake_time):
85 entry.flags |= FLAG_SOURCE_MODIFIED 142 entry.flags |= BakeRecordPageEntry.FLAG_SOURCE_MODIFIED
143 self.dirty_source_names.add(entry.source_name)
86 super(TransitionalBakeRecord, self).addEntry(entry) 144 super(TransitionalBakeRecord, self).addEntry(entry)
87 145
88 def getTransitionKey(self, entry): 146 def getTransitionKey(self, entry):
89 return _get_transition_key(entry.source_name, entry.rel_path, 147 return _get_transition_key(entry.source_name, entry.rel_path,
90 entry.taxonomy_info) 148 entry.taxonomy_info)
91 149
92 def getOverrideEntry(self, factory, uri): 150 def getOverrideEntry(self, factory, uri):
93 for pair in self.transitions.values(): 151 for pair in self.transitions.values():
94 prev = pair[0]
95 cur = pair[1] 152 cur = pair[1]
96 if (cur and 153 if (cur and
97 (cur.source_name != factory.source.name or 154 (cur.source_name != factory.source.name or
98 cur.rel_path != factory.rel_path) and 155 cur.rel_path != factory.rel_path)):
99 len(cur.out_uris) > 0 and cur.out_uris[0] == uri): 156 for o in cur.subs:
100 return cur 157 if o.out_uri == uri:
101 if (prev and 158 return cur
102 (prev.source_name != factory.source.name or
103 prev.rel_path != factory.rel_path) and
104 len(prev.out_uris) > 0 and prev.out_uris[0] == uri):
105 return prev
106 return None 159 return None
107 160
108 def getPreviousEntry(self, source_name, rel_path, taxonomy_info=None): 161 def getPreviousEntry(self, source_name, rel_path, taxonomy_info=None):
109 key = _get_transition_key(source_name, rel_path, taxonomy_info) 162 key = _get_transition_key(source_name, rel_path, taxonomy_info)
110 pair = self.transitions.get(key) 163 pair = self.transitions.get(key)
111 if pair is not None: 164 if pair is not None:
112 return pair[0] 165 return pair[0]
113 return None 166 return None
114 167
115 def getCurrentEntries(self, source_name): 168 def collapseEntry(self, prev_entry):
116 return [e for e in self.current.entries 169 cur_entry = copy.deepcopy(prev_entry)
117 if e.source_name == source_name] 170 cur_entry.flags = BakeRecordPageEntry.FLAG_NONE
118 171 for o in cur_entry.subs:
119 def collapseRecords(self): 172 o.flags = BakeRecordSubPageEntry.FLAG_NONE
120 for prev, cur in self.transitions.values(): 173 self.addEntry(cur_entry)
121 if prev and cur and not cur.was_baked:
122 # This page wasn't baked, so the information from last
123 # time is still valid (we didn't get any information
124 # since we didn't bake).
125 cur.flags = prev.flags
126 if prev.config:
127 cur.config = prev.config.copy()
128 cur.out_uris = list(prev.out_uris)
129 cur.out_paths = list(prev.out_paths)
130 cur.errors = list(prev.errors)
131 cur.used_source_names = set(prev.used_source_names)
132 cur.used_taxonomy_terms = set(prev.used_taxonomy_terms)
133 174
134 def getDeletions(self): 175 def getDeletions(self):
135 for prev, cur in self.transitions.values(): 176 for prev, cur in self.transitions.values():
136 if prev and not cur: 177 if prev and not cur:
137 for p in prev.out_paths: 178 for sub in prev.subs:
138 yield (p, 'previous source file was removed') 179 yield (sub.out_path, 'previous source file was removed')
139 elif prev and cur and cur.was_baked_successfully: 180 elif prev and cur:
140 diff = set(prev.out_paths) - set(cur.out_paths) 181 prev_out_paths = [o.out_path for o in prev.subs]
182 cur_out_paths = [o.out_path for o in cur.subs]
183 diff = set(prev_out_paths) - set(cur_out_paths)
141 for p in diff: 184 for p in diff:
142 yield (p, 'source file changed outputs') 185 yield (p, 'source file changed outputs')
143 186
187 def _onNewEntryAdded(self, entry):
188 entry.flags |= BakeRecordPageEntry.FLAG_NEW
189