forked from hyde/hyde
-
Notifications
You must be signed in to change notification settings - Fork 0
/
generator.py
346 lines (305 loc) · 12.2 KB
/
generator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# -*- coding: utf-8 -*-
"""
The generator class and related utility functions.
"""
from hyde.exceptions import HydeException
from hyde.fs import File, Folder
from hyde.model import Context, Dependents
from hyde.plugin import Plugin
from hyde.template import Template
from hyde.site import Node, Resource
from contextlib import contextmanager
from datetime import datetime
from shutil import copymode
from hyde.util import getLoggerWithNullHandler
logger = getLoggerWithNullHandler('hyde.engine')
class Generator(object):
"""
Generates output from a node or resource.
"""
def __init__(self, site):
super(Generator, self).__init__()
self.site = site
self.generated_once = False
self.deps = Dependents(site.sitepath)
self.create_context()
self.template = None
Plugin.load_all(site)
self.events = Plugin.get_proxy(self.site)
def create_context(self):
site = self.site
self.__context__ = dict(site=site)
if hasattr(site.config, 'context'):
site.context = Context.load(site.sitepath, site.config.context)
self.__context__.update(site.context)
@contextmanager
def context_for_resource(self, resource):
"""
Context manager that intializes the context for a given
resource and rolls it back after the resource is processed.
"""
self.__context__.update(
resource=resource,
node=resource.node,
time_now=datetime.now())
yield self.__context__
self.__context__.update(resource=None, node=None)
def context_for_path(self, path):
resource = self.site.resource_from_path(path)
if not resource:
return {}
ctx = self.__context__.copy
ctx.resource = resource
return ctx
def load_template_if_needed(self):
"""
Loads and configures the template environement from the site
configuration if its not done already.
"""
class GeneratorProxy(object):
"""
An interface to templates and plugins for
providing restricted access to the methods.
"""
def __init__(self, preprocessor=None, postprocessor=None, context_for_path=None):
self.preprocessor = preprocessor
self.postprocessor = postprocessor
self.context_for_path = context_for_path
if not self.template:
logger.info("Generating site at [%s]" % self.site.sitepath)
self.template = Template.find_template(self.site)
logger.debug("Using [%s] as the template",
self.template.__class__.__name__)
logger.info("Configuring the template environment")
self.template.configure(self.site,
engine=GeneratorProxy(
context_for_path=self.context_for_path,
preprocessor=self.events.begin_text_resource,
postprocessor=self.events.text_resource_complete))
self.events.template_loaded(self.template)
def initialize(self):
"""
Start Generation. Perform setup tasks and inform plugins.
"""
logger.debug("Begin Generation")
self.events.begin_generation()
def load_site_if_needed(self):
"""
Checks if the site requries a reload and loads if
necessary.
"""
self.site.reload_if_needed()
def finalize(self):
"""
Generation complete. Inform plugins and cleanup.
"""
logger.debug("Generation Complete")
self.events.generation_complete()
def get_dependencies(self, resource):
"""
Gets the dependencies for a given resource.
"""
rel_path = resource.relative_path
deps = self.deps[rel_path] if rel_path in self.deps \
else self.update_deps(resource)
return deps
def update_deps(self, resource):
"""
Updates the dependencies for the given resource.
"""
if not resource.source_file.is_text:
return []
rel_path = resource.relative_path
deps = []
if hasattr(resource, 'depends'):
user_deps = resource.depends
for dep in user_deps:
deps.append(dep)
dep_res = self.site.content.resource_from_relative_path(dep)
if dep_res:
deps.extend(self.get_dependencies(dep_res))
if resource.uses_template:
deps.extend(self.template.get_dependencies(rel_path))
deps = list(set(deps))
if None in deps:
deps.remove(None)
self.deps[rel_path] = deps
return deps
def has_resource_changed(self, resource):
"""
Checks if the given resource has changed since the
last generation.
"""
logger.debug("Checking for changes in %s" % resource)
self.load_template_if_needed()
self.load_site_if_needed()
target = File(self.site.config.deploy_root_path.child(
resource.relative_deploy_path))
if not target.exists or target.older_than(resource.source_file):
logger.debug("Found changes in %s" % resource)
return True
if resource.source_file.is_binary:
logger.debug("No Changes found in %s" % resource)
return False
if self.site.config.needs_refresh() or \
not target.has_changed_since(self.site.config.last_modified):
logger.debug("Site configuration changed")
return True
deps = self.get_dependencies(resource)
if not deps or None in deps:
logger.debug("No changes found in %s" % resource)
return False
content = self.site.content.source_folder
layout = Folder(self.site.sitepath).child_folder('layout')
logger.debug("Checking for changes in dependents:%s" % deps)
for dep in deps:
if not dep:
return True
source = File(content.child(dep))
if not source.exists:
source = File(layout.child(dep))
if not source.exists:
return True
if target.older_than(source):
return True
logger.debug("No changes found in %s" % resource)
return False
def generate_all(self, incremental=False):
"""
Generates the entire website
"""
logger.info("Reading site contents")
self.load_template_if_needed()
self.template.clear_caches()
self.initialize()
self.load_site_if_needed()
self.events.begin_site()
logger.info("Generating site to [%s]" %
self.site.config.deploy_root_path)
self.__generate_node__(self.site.content, incremental)
self.events.site_complete()
self.finalize()
self.generated_once = True
def generate_node_at_path(self, node_path=None, incremental=False):
"""
Generates a single node. If node_path is non-existent or empty,
generates the entire site.
"""
if not self.generated_once and not incremental:
return self.generate_all()
self.load_template_if_needed()
self.load_site_if_needed()
node = None
if node_path:
node = self.site.content.node_from_path(node_path)
self.generate_node(node, incremental)
@contextmanager
def events_for(self, obj):
if not self.generated_once:
self.events.begin_site()
if isinstance(obj, Resource):
self.events.begin_node(obj.node)
yield
if not self.generated_once:
if isinstance(obj, Resource):
self.events.node_complete(obj.node)
self.events.site_complete()
self.generated_once = True
def generate_node(self, node=None, incremental=False):
"""
Generates the given node. If node is invalid, empty or
non-existent, generates the entire website.
"""
if not node or not self.generated_once and not incremental:
return self.generate_all()
self.load_template_if_needed()
self.initialize()
self.load_site_if_needed()
try:
with self.events_for(node):
self.__generate_node__(node, incremental)
self.finalize()
except HydeException:
self.generate_all()
def generate_resource_at_path(self,
resource_path=None,
incremental=False):
"""
Generates a single resource. If resource_path is non-existent or empty,
generats the entire website.
"""
if not self.generated_once and not incremental:
return self.generate_all()
self.load_template_if_needed()
self.load_site_if_needed()
resource = None
if resource_path:
resource = self.site.content.resource_from_path(resource_path)
self.generate_resource(resource, incremental)
def generate_resource(self, resource=None, incremental=False):
"""
Generates the given resource. If resource is invalid, empty or
non-existent, generates the entire website.
"""
if not resource or not self.generated_once and not incremental:
return self.generate_all()
self.load_template_if_needed()
self.initialize()
self.load_site_if_needed()
try:
with self.events_for(resource):
self.__generate_resource__(resource, incremental)
except HydeException:
self.generate_all()
def refresh_config(self):
if self.site.config.needs_refresh():
logger.debug("Refreshing configuration and context")
self.site.refresh_config()
self.create_context()
def __generate_node__(self, node, incremental=False):
self.refresh_config()
for node in node.walk():
logger.debug("Generating Node [%s]", node)
self.events.begin_node(node)
for resource in node.resources:
self.__generate_resource__(resource, incremental)
self.events.node_complete(node)
def __generate_resource__(self, resource, incremental=False):
self.refresh_config()
if not resource.is_processable:
logger.debug("Skipping [%s]", resource)
return
if incremental and not self.has_resource_changed(resource):
logger.debug("No changes found. Skipping resource [%s]", resource)
return
logger.debug("Processing [%s]", resource)
with self.context_for_resource(resource) as context:
target = File(self.site.config.deploy_root_path.child(
resource.relative_deploy_path))
target.parent.make()
if resource.simple_copy:
logger.debug("Simply Copying [%s]", resource)
resource.source_file.copy_to(target)
elif resource.source_file.is_text:
self.update_deps(resource)
if resource.uses_template:
logger.debug("Rendering [%s]", resource)
try:
text = self.template.render_resource(resource,
context)
except Exception:
logger.error("Error occurred when"
" processing template: [%s]" % resource)
raise
else:
text = resource.source_file.read_all()
text = self.events.begin_text_resource(resource, text) or text
text = self.events.text_resource_complete(
resource, text) or text
target.write(text)
copymode(resource.source_file.path, target.path)
else:
logger.debug("Copying binary file [%s]", resource)
self.events.begin_binary_resource(resource)
resource.source_file.copy_to(target)
self.events.binary_resource_complete(resource)