PageRenderTime 64ms CodeModel.GetById 43ms app.highlight 17ms RepoModel.GetById 1ms app.codeStats 0ms

/hyde/generator.py

http://github.com/hyde/hyde
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)