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