From acdc31cf3f96ea42313319a55a0bcdbed6dfc2c0 Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Sun, 10 Jul 2011 14:54:03 +1000 Subject: [PATCH 1/7] adding preliminary "sphinx" plugin, for auto-generated docs --- hyde/ext/plugins/sphinx.py | 123 +++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 hyde/ext/plugins/sphinx.py diff --git a/hyde/ext/plugins/sphinx.py b/hyde/ext/plugins/sphinx.py new file mode 100644 index 0000000..2f90592 --- /dev/null +++ b/hyde/ext/plugins/sphinx.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +""" +Sphinx plugin. + +This plugin lets you easily include sphinx-generated documentation as part +of your Hyde site. +""" + +from __future__ import absolute_import + +import json +import tempfile + +from hyde.plugin import Plugin +from hyde.fs import File, Folder +from hyde.model import Expando + +import sphinx + + +class SphinxPlugin(Plugin): + """The plugin class for rendering sphinx-generated documentation.""" + + def __init__(self, site): + self.sphinx_build_dir = None + super(SphinxPlugin, self).__init__(site) + + @property + def plugin_name(self): + return "sphinx" + + @property + def settings(self): + settings = Expando({}) + settings.conf_path = "." + settings.block_map = Expando({}) + settings.block_map.body = "body" + try: + user_settings = getattr(self.site.config, self.plugin_name) + except AttributeError: + pass + else: + for name in dir(user_settings): + if not name.startswith("_"): + setattr(settings,name,getattr(user_settings,name)) + return settings + + def begin_site(self): + # 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 + for resource in self.site.content.walk_resources(): + if resource.source_file.kind == "rst": + 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 + + def begin_text_resource(self,resource,text): + """If this is a sphinx input file, replace it with the generated docs. + + 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": + 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 %}") + return "\n".join(output) + + def site_complete(self): + if self.sphinx_build_dir is not None: + self.sphinx_build_dir.delete() + + def _run_sphinx(self): + """Run sphinx to generate the necessary output files. + + This method creates a temporary directory, copies all sphinx-related + files into it, and then runs the sphinx build process to produce + the documentation fragments. + """ + self.sphinx_build_dir = Folder(tempfile.mkdtemp()) + # Copy all sphinx files into a temporary build directory. + self.sphinx_input_dir = self.sphinx_build_dir.child_folder("input") + self.sphinx_input_dir.make() + for resource in self.site.content.walk_resources(): + if resource.source_file.kind == "rst": + relpath = resource.relative_path + build_file = File(self.sphinx_input_dir.child(relpath)) + build_file.parent.make() + resource.source_file.copy_to(build_file) + # Run sphinx to generate output in the output directory. + self.sphinx_output_dir = self.sphinx_build_dir.child_folder("output") + self.sphinx_output_dir.make() + conf_path = self.site.sitepath.child_folder(self.settings.conf_path) + sphinx_args = ["sphinx-build"] + sphinx_args.extend([ + "-b", "json", + "-c", conf_path.path, + self.sphinx_input_dir.path, + self.sphinx_output_dir.path + ]) + if sphinx.main(sphinx_args) != 0: + raise RuntimeError("sphinx build failed") + + def _get_sphinx_output(self,resource): + relpath = File(resource.relative_path) + relpath = relpath.parent.child(relpath.name_without_extension+".fjson") + with open(self.sphinx_output_dir.child(relpath),"rb") as f: + return json.load(f) + From 43e36de3360be1a83860c01b5c017b1df2bddac4 Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Sun, 10 Jul 2011 15:13:05 +1000 Subject: [PATCH 2/7] SphinxPlugin: run sphinx-build directly against content dir --- hyde/ext/plugins/sphinx.py | 23 +++++------------------ hyde/tests/ext/optipng/hyde-lt-b.png | Bin 35 -> 8116 bytes 2 files changed, 5 insertions(+), 18 deletions(-) mode change 120000 => 100644 hyde/tests/ext/optipng/hyde-lt-b.png diff --git a/hyde/ext/plugins/sphinx.py b/hyde/ext/plugins/sphinx.py index 2f90592..3ccf383 100644 --- a/hyde/ext/plugins/sphinx.py +++ b/hyde/ext/plugins/sphinx.py @@ -87,30 +87,17 @@ class SphinxPlugin(Plugin): def _run_sphinx(self): """Run sphinx to generate the necessary output files. - This method creates a temporary directory, copies all sphinx-related - files into it, and then runs the sphinx build process to produce - the documentation fragments. + This method creates a temporary directory for sphinx's output, then + run sphinx against the Hyde input directory. """ self.sphinx_build_dir = Folder(tempfile.mkdtemp()) - # Copy all sphinx files into a temporary build directory. - self.sphinx_input_dir = self.sphinx_build_dir.child_folder("input") - self.sphinx_input_dir.make() - for resource in self.site.content.walk_resources(): - if resource.source_file.kind == "rst": - relpath = resource.relative_path - build_file = File(self.sphinx_input_dir.child(relpath)) - build_file.parent.make() - resource.source_file.copy_to(build_file) - # Run sphinx to generate output in the output directory. - self.sphinx_output_dir = self.sphinx_build_dir.child_folder("output") - self.sphinx_output_dir.make() conf_path = self.site.sitepath.child_folder(self.settings.conf_path) sphinx_args = ["sphinx-build"] sphinx_args.extend([ "-b", "json", "-c", conf_path.path, - self.sphinx_input_dir.path, - self.sphinx_output_dir.path + self.site.content.path, + self.sphinx_build_dir.path ]) if sphinx.main(sphinx_args) != 0: raise RuntimeError("sphinx build failed") @@ -118,6 +105,6 @@ class SphinxPlugin(Plugin): def _get_sphinx_output(self,resource): relpath = File(resource.relative_path) relpath = relpath.parent.child(relpath.name_without_extension+".fjson") - with open(self.sphinx_output_dir.child(relpath),"rb") as f: + with open(self.sphinx_build_dir.child(relpath),"rb") as f: return json.load(f) diff --git a/hyde/tests/ext/optipng/hyde-lt-b.png b/hyde/tests/ext/optipng/hyde-lt-b.png deleted file mode 120000 index 30dbddc..0000000 --- a/hyde/tests/ext/optipng/hyde-lt-b.png +++ /dev/null @@ -1 +0,0 @@ -../../../../resources/hyde-lt-b.png \ No newline at end of file diff --git a/hyde/tests/ext/optipng/hyde-lt-b.png b/hyde/tests/ext/optipng/hyde-lt-b.png new file mode 100644 index 0000000000000000000000000000000000000000..09c383a3d7fc0d95d5029e01fbcfe15586f22b59 GIT binary patch literal 8116 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 Date: Sun, 10 Jul 2011 21:59:58 +1000 Subject: [PATCH 3/7] add a custom sphinx builder, so we can get the internal links right --- hyde/ext/plugins/sphinx.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/hyde/ext/plugins/sphinx.py b/hyde/ext/plugins/sphinx.py index 3ccf383..ffa9ce9 100644 --- a/hyde/ext/plugins/sphinx.py +++ b/hyde/ext/plugins/sphinx.py @@ -16,6 +16,8 @@ from hyde.fs import File, Folder from hyde.model import Expando import sphinx +from sphinx.builders.html import JSONHTMLBuilder +from sphinx.util.osutil import SEP class SphinxPlugin(Plugin): @@ -94,7 +96,7 @@ class SphinxPlugin(Plugin): conf_path = self.site.sitepath.child_folder(self.settings.conf_path) sphinx_args = ["sphinx-build"] sphinx_args.extend([ - "-b", "json", + "-b", "hyde_json", "-c", conf_path.path, self.site.content.path, self.sphinx_build_dir.path @@ -108,3 +110,15 @@ class SphinxPlugin(Plugin): with open(self.sphinx_build_dir.child(relpath),"rb") as f: return json.load(f) + + +class HydeJSONHTMLBuilder(JSONHTMLBuilder): + name = "hyde_json" + def get_target_uri(self, docname, typ=None): + return docname + ".html" + + +def setup(app): + app.add_builder(HydeJSONHTMLBuilder) + + From d905d573816732789ff8a4cb5d364c2cc8a06475 Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Mon, 11 Jul 2011 09:39:26 +1000 Subject: [PATCH 4/7] add some sanity-checks to the sphinx plugin --- hyde/ext/plugins/sphinx.py | 119 ++++++++++++++++++++++++++++++++----- 1 file changed, 104 insertions(+), 15 deletions(-) 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"] From 1aed5875f76bf2be9ae67f963251117c159d1a04 Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Mon, 11 Jul 2011 09:54:13 +1000 Subject: [PATCH 5/7] sphinx plugin: add some docs and extra sanity checks --- hyde/ext/plugins/sphinx.py | 57 +++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/hyde/ext/plugins/sphinx.py b/hyde/ext/plugins/sphinx.py index 78cefe0..05d0c7f 100644 --- a/hyde/ext/plugins/sphinx.py +++ b/hyde/ext/plugins/sphinx.py @@ -3,7 +3,7 @@ Sphinx plugin. This plugin lets you easily include sphinx-generated documentation as part -of your Hyde site. +of your Hyde site. It is simultaneously a Hyde plugin and a Sphinx plugin. """ # We need absolute import so that we can import the main "sphinx" @@ -18,6 +18,7 @@ import tempfile from hyde.plugin import Plugin from hyde.fs import File, Folder from hyde.model import Expando +from hyde.ext.plugins.meta import MetaPlugin as _MetaPlugin import sphinx from sphinx.builders.html import JSONHTMLBuilder @@ -85,6 +86,12 @@ class SphinxPlugin(Plugin): return self._sphinx_config def begin_site(self): + """Event hook for when site processing begins. + + This hook checks that the site is correctly configured for building + with sphinx, and adjusts any sphinx-controlled resources so that + hyde will process them correctly. + """ settings = self.settings if settings.sanity_check: self._sanity_check() @@ -102,10 +109,14 @@ class SphinxPlugin(Plugin): 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. + """Event hook for processing an individual resource. + + If the input resource is a sphinx input file, this method will replace + replace the text of the file with the sphinx-generated documentation. - This method will replace the text of the file with the sphinx-generated - documentation, lazily running sphinx if it has not yet been called. + Sphinx itself is run lazily the first time this method is called. + This means that if no sphinx-related resources need updating, then + we entirely avoid running sphinx. """ suffix = self.sphinx_config.get("source_suffix",".rst") if not resource.source_file.path.endswith(suffix): @@ -132,6 +143,10 @@ class SphinxPlugin(Plugin): return "\n".join(output) def site_complete(self): + """Event hook for when site processing ends. + + This simply cleans up any temorary build file. + """ if self.sphinx_build_dir is not None: self.sphinx_build_dir.delete() @@ -173,6 +188,19 @@ class SphinxPlugin(Plugin): logger.error("sphinx conf.py file.") logger.info("(set sphinx.sanity_check=false to disable this check)") raise RuntimeError("sphinx is not configured correctly") + # Check that I am *before* the other plugins, + # with the possible exception of MetaPlugin + for plugin in self.site.plugins: + if plugin is self: + break + if not isinstance(plugin,_MetaPlugin): + logger.error("The sphinx plugin is installed after the") + logger.error("plugin %r.",plugin.__class__.__name__) + logger.error("It's quite likely that this will break things.") + logger.error("Please move the sphinx plugin to the top") + logger.error("of the plugins list.") + logger.info("(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. @@ -194,6 +222,13 @@ class SphinxPlugin(Plugin): raise RuntimeError("sphinx build failed") def _get_sphinx_output(self,resource): + """Get the sphinx output for a given resource. + + This returns a dict mapping block names to HTML text fragments. + The most important fragment is "body" which contains the main text + of the document. The other fragments are for things like navigation, + related pages and so-on. + """ relpath = File(resource.relative_path) relpath = relpath.parent.child(relpath.name_without_extension+".fjson") with open(self.sphinx_build_dir.child(relpath),"rb") as f: @@ -202,12 +237,26 @@ class SphinxPlugin(Plugin): class HydeJSONHTMLBuilder(JSONHTMLBuilder): + """A slightly-customised JSONHTMLBuilder, for use by Hyde. + + This is a Sphinx builder that serilises the generated HTML fragments into + a JSON docuent, so they can be later retrieved and dealt with at will. + + The only customistion we do over the standard JSONHTMLBuilder is to + reference documents with a .html suffix, so that internal link will + work correctly once things have been processed by Hyde. + """ name = "hyde_json" def get_target_uri(self, docname, typ=None): return docname + ".html" def setup(app): + """Sphinx plugin setup function. + + This function allows the module to act as a Sphinx plugin as well as a + Hyde plugin. It simply registers the HydeJSONHTMLBuilder class. + """ app.add_builder(HydeJSONHTMLBuilder) From 566c1f103ea15f46fe7c0f0ebc94e1a8d407e92d Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Mon, 11 Jul 2011 10:02:03 +1000 Subject: [PATCH 6/7] sphinx/pyfs plugins: add error messages for missing imports --- hyde/ext/plugins/sphinx.py | 12 ++++++++---- hyde/ext/publishers/pyfs.py | 11 ++++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/hyde/ext/plugins/sphinx.py b/hyde/ext/plugins/sphinx.py index 05d0c7f..9eba421 100644 --- a/hyde/ext/plugins/sphinx.py +++ b/hyde/ext/plugins/sphinx.py @@ -20,13 +20,17 @@ from hyde.fs import File, Folder from hyde.model import Expando from hyde.ext.plugins.meta import MetaPlugin as _MetaPlugin -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') +try: + import sphinx + from sphinx.builders.html import JSONHTMLBuilder + from sphinx.util.osutil import SEP +except ImportError: + logger.error("The sphinx plugin requires sphinx.") + logger.error("`pip install -U sphinx` to get it.") + raise class SphinxPlugin(Plugin): diff --git a/hyde/ext/publishers/pyfs.py b/hyde/ext/publishers/pyfs.py index 9431afb..5a9b4ff 100644 --- a/hyde/ext/publishers/pyfs.py +++ b/hyde/ext/publishers/pyfs.py @@ -21,9 +21,14 @@ from hyde.util import getLoggerWithNullHandler logger = getLoggerWithNullHandler('hyde.ext.publishers.pyfs') -from fs.osfs import OSFS -from fs.path import pathjoin -from fs.opener import fsopendir +try: + from fs.osfs import OSFS + from fs.path import pathjoin + from fs.opener import fsopendir +except ImportError: + logger.error("The PyFS publisher requires PyFilesystem v0.4 or later.") + logger.error("`pip install -U fs` to get it.") + raise From d6574bb17cfc4c6bec81696365de279324703c3e Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Mon, 11 Jul 2011 10:13:19 +1000 Subject: [PATCH 7/7] sphinx plugin: add some high-level docs --- hyde/ext/plugins/sphinx.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/hyde/ext/plugins/sphinx.py b/hyde/ext/plugins/sphinx.py index 9eba421..9f342b0 100644 --- a/hyde/ext/plugins/sphinx.py +++ b/hyde/ext/plugins/sphinx.py @@ -4,6 +4,36 @@ Sphinx plugin. This plugin lets you easily include sphinx-generated documentation as part of your Hyde site. It is simultaneously a Hyde plugin and a Sphinx plugin. + +To make this work, you need to: + + * install sphinx, obviously + * include your sphinx source files in the Hyde source tree + * put the sphinx conf.py file in the Hyde site directory + * point conf.py:master_doc at an appropriate file in the source tree + +For example you might have your site set up like this:: + + site.yaml <-- hyde config file + conf.py <-- sphinx config file + contents/ + index.html <-- non-sphinx files, handled by hyde + other.html + api/ + index.rst <-- files to processed by sphinx + mymodule.rst + +When the site is built, the .rst files will first be processed by sphinx +to generate a HTML docuent, which will then be passed through the normal +hyde templating workflow. You would end up with:: + + deploy/ + index.html <-- files generated by hyde + other.html + api/ + index.html <-- files generated by sphinx, then hyde + mymodule.html + """ # We need absolute import so that we can import the main "sphinx"