diff --git a/hyde/ext/plugins/sphinx.py b/hyde/ext/plugins/sphinx.py index ffa9ce9..78cefe0 100644 --- a/hyde/ext/plugins/sphinx.py +++ b/hyde/ext/plugins/sphinx.py @@ -6,8 +6,12 @@ This plugin lets you easily include sphinx-generated documentation as part of your Hyde site. """ +# We need absolute import so that we can import the main "sphinx" +# module even though this module is also called "sphinx". Ugh. from __future__ import absolute_import +import os +import sys import json import tempfile @@ -19,24 +23,35 @@ import sphinx from sphinx.builders.html import JSONHTMLBuilder from sphinx.util.osutil import SEP +from hyde.util import getLoggerWithNullHandler +logger = getLoggerWithNullHandler('hyde.ext.plugins.sphinx') + + class SphinxPlugin(Plugin): """The plugin class for rendering sphinx-generated documentation.""" def __init__(self, site): self.sphinx_build_dir = None + self._sphinx_config = None super(SphinxPlugin, self).__init__(site) @property def plugin_name(self): + """The name of the plugin, obivously.""" return "sphinx" @property def settings(self): + """Settings for this plugin. + + This property combines default settings with those specified in the + site config to produce the final settings for this plugin. + """ settings = Expando({}) + settings.sanity_check = True settings.conf_path = "." - settings.block_map = Expando({}) - settings.block_map.body = "body" + settings.block_map = {} try: user_settings = getattr(self.site.config, self.plugin_name) except AttributeError: @@ -47,17 +62,44 @@ class SphinxPlugin(Plugin): setattr(settings,name,getattr(user_settings,name)) return settings + @property + def sphinx_config(self): + """Configuration options for sphinx. + + This is a lazily-generated property giving the options from the + sphinx configuration file. It's generated by actualy executing + the config file, so don't do anything silly in there. + """ + if self._sphinx_config is None: + conf_path = self.settings.conf_path + conf_path = self.site.sitepath.child_folder(conf_path) + # Sphinx always execs the config file in its parent dir. + conf_file = conf_path.child("conf.py") + self._sphinx_config = {"__file__":conf_file} + curdir = os.getcwd() + os.chdir(conf_path.path) + try: + execfile(conf_file,self._sphinx_config) + finally: + os.chdir(curdir) + return self._sphinx_config + def begin_site(self): + settings = self.settings + if settings.sanity_check: + self._sanity_check() # Find and adjust all the resource that will be handled by sphinx. # We need to: # * change the deploy name from .rst to .html - # * make sure they don't get rendered inside a default block + # * if a block_map is given, switch off default_block + suffix = self.sphinx_config.get("source_suffix",".rst") for resource in self.site.content.walk_resources(): - if resource.source_file.kind == "rst": + if resource.source_file.path.endswith(suffix): new_name = resource.source_file.name_without_extension + ".html" target_folder = File(resource.relative_deploy_path).parent resource.relative_deploy_path = target_folder.child(new_name) - resource.meta.default_block = None + if settings.block_map: + resource.meta.default_block = None def begin_text_resource(self,resource,text): """If this is a sphinx input file, replace it with the generated docs. @@ -65,33 +107,80 @@ class SphinxPlugin(Plugin): This method will replace the text of the file with the sphinx-generated documentation, lazily running sphinx if it has not yet been called. """ - if resource.source_file.kind != "rst": + suffix = self.sphinx_config.get("source_suffix",".rst") + if not resource.source_file.path.endswith(suffix): return text if self.sphinx_build_dir is None: self._run_sphinx() output = [] settings = self.settings - for (nm,content) in self._get_sphinx_output(resource).iteritems(): - try: - block = getattr(settings.block_map,nm) - except AttributeError: - pass - else: - output.append("{%% block %s %%}" % (block,)) - output.append(content) - output.append("{% endblock %}") + sphinx_output = self._get_sphinx_output(resource) + # If they're set up a block_map, use the specific blocks. + # Otherwise, output just the body for use by default_block. + if not settings.block_map: + output.append(sphinx_output["body"]) + else: + for (nm,content) in sphinx_output.iteritems(): + try: + block = getattr(settings.block_map,nm) + except AttributeError: + pass + else: + output.append("{%% block %s %%}" % (block,)) + output.append(content) + output.append("{% endblock %}") return "\n".join(output) def site_complete(self): if self.sphinx_build_dir is not None: self.sphinx_build_dir.delete() + def _sanity_check(self): + """Check the current site for sanity. + + This method checks that the site is propertly set up for building + things with sphinx, e.g. it has a config file, a master document, + the hyde sphinx extension is enabled, and so-on. + """ + # Check that the sphinx config file actually exists. + try: + sphinx_config = self.sphinx_config + except EnvironmentError: + logger.error("Could not read the sphinx config file.") + conf_path = self.settings.conf_path + conf_path = self.site.sitepath.child_folder(conf_path) + conf_file = conf_path.child("conf.py") + logger.error("Please ensure %s is a valid sphinx config",conf_file) + logger.error("or set sphinx.conf_path to the directory") + logger.error("containing your sphinx conf.py") + raise + # Check that the hyde_json extension is loaded + extensions = sphinx_config.get("extensions",[]) + if "hyde.ext.plugins.sphinx" not in extensions: + logger.error("The hyde_json sphinx extension is not configured.") + logger.error("Please add 'hyde.ext.plugins.sphinx' to the list") + logger.error("of extensions in your sphinx conf.py file.") + logger.info("(set sphinx.sanity_check=false to disable this check)") + raise RuntimeError("sphinx is not configured correctly") + # Check that the master doc exists in the source tree. + master_doc = sphinx_config.get("master_doc","index") + master_doc += sphinx_config.get("source_suffix",".rst") + master_doc = os.path.join(self.site.content.path,master_doc) + if not os.path.exists(master_doc): + logger.error("The sphinx master document doesn't exist.") + logger.error("Please create the file %s",master_doc) + logger.error("or change the 'master_doc' setting in your") + logger.error("sphinx conf.py file.") + logger.info("(set sphinx.sanity_check=false to disable this check)") + raise RuntimeError("sphinx is not configured correctly") + def _run_sphinx(self): """Run sphinx to generate the necessary output files. This method creates a temporary directory for sphinx's output, then run sphinx against the Hyde input directory. """ + logger.info("running sphinx") self.sphinx_build_dir = Folder(tempfile.mkdtemp()) conf_path = self.site.sitepath.child_folder(self.settings.conf_path) sphinx_args = ["sphinx-build"]