| @@ -6,8 +6,12 @@ This plugin lets you easily include sphinx-generated documentation as part | |||||
| of your Hyde site. | 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 | from __future__ import absolute_import | ||||
| import os | |||||
| import sys | |||||
| import json | import json | ||||
| import tempfile | import tempfile | ||||
| @@ -19,24 +23,35 @@ import sphinx | |||||
| from sphinx.builders.html import JSONHTMLBuilder | from sphinx.builders.html import JSONHTMLBuilder | ||||
| from sphinx.util.osutil import SEP | from sphinx.util.osutil import SEP | ||||
| from hyde.util import getLoggerWithNullHandler | |||||
| logger = getLoggerWithNullHandler('hyde.ext.plugins.sphinx') | |||||
| class SphinxPlugin(Plugin): | class SphinxPlugin(Plugin): | ||||
| """The plugin class for rendering sphinx-generated documentation.""" | """The plugin class for rendering sphinx-generated documentation.""" | ||||
| def __init__(self, site): | def __init__(self, site): | ||||
| self.sphinx_build_dir = None | self.sphinx_build_dir = None | ||||
| self._sphinx_config = None | |||||
| super(SphinxPlugin, self).__init__(site) | super(SphinxPlugin, self).__init__(site) | ||||
| @property | @property | ||||
| def plugin_name(self): | def plugin_name(self): | ||||
| """The name of the plugin, obivously.""" | |||||
| return "sphinx" | return "sphinx" | ||||
| @property | @property | ||||
| def settings(self): | 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 = Expando({}) | ||||
| settings.sanity_check = True | |||||
| settings.conf_path = "." | settings.conf_path = "." | ||||
| settings.block_map = Expando({}) | |||||
| settings.block_map.body = "body" | |||||
| settings.block_map = {} | |||||
| try: | try: | ||||
| user_settings = getattr(self.site.config, self.plugin_name) | user_settings = getattr(self.site.config, self.plugin_name) | ||||
| except AttributeError: | except AttributeError: | ||||
| @@ -47,17 +62,44 @@ class SphinxPlugin(Plugin): | |||||
| setattr(settings,name,getattr(user_settings,name)) | setattr(settings,name,getattr(user_settings,name)) | ||||
| return settings | 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): | 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. | # Find and adjust all the resource that will be handled by sphinx. | ||||
| # We need to: | # We need to: | ||||
| # * change the deploy name from .rst to .html | # * 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(): | 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" | new_name = resource.source_file.name_without_extension + ".html" | ||||
| target_folder = File(resource.relative_deploy_path).parent | target_folder = File(resource.relative_deploy_path).parent | ||||
| resource.relative_deploy_path = target_folder.child(new_name) | 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): | def begin_text_resource(self,resource,text): | ||||
| """If this is a sphinx input file, replace it with the generated docs. | """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 | This method will replace the text of the file with the sphinx-generated | ||||
| documentation, lazily running sphinx if it has not yet been called. | 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 | return text | ||||
| if self.sphinx_build_dir is None: | if self.sphinx_build_dir is None: | ||||
| self._run_sphinx() | self._run_sphinx() | ||||
| output = [] | output = [] | ||||
| settings = self.settings | 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) | return "\n".join(output) | ||||
| def site_complete(self): | def site_complete(self): | ||||
| if self.sphinx_build_dir is not None: | if self.sphinx_build_dir is not None: | ||||
| self.sphinx_build_dir.delete() | 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): | def _run_sphinx(self): | ||||
| """Run sphinx to generate the necessary output files. | """Run sphinx to generate the necessary output files. | ||||
| This method creates a temporary directory for sphinx's output, then | This method creates a temporary directory for sphinx's output, then | ||||
| run sphinx against the Hyde input directory. | run sphinx against the Hyde input directory. | ||||
| """ | """ | ||||
| logger.info("running sphinx") | |||||
| self.sphinx_build_dir = Folder(tempfile.mkdtemp()) | self.sphinx_build_dir = Folder(tempfile.mkdtemp()) | ||||
| conf_path = self.site.sitepath.child_folder(self.settings.conf_path) | conf_path = self.site.sitepath.child_folder(self.settings.conf_path) | ||||
| sphinx_args = ["sphinx-build"] | sphinx_args = ["sphinx-build"] | ||||