A fork of hyde, the static site generation. Some patches will be pushed upstream.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

354 lines
12 KiB

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