/hyde/generator.py

http://github.com/hyde/hyde · Python · 357 lines · 298 code · 11 blank · 48 comment · 9 complexity · 93c506f9bc7fe4f606753b831098172f MD5 · raw file

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