# -*- coding: utf-8 -*- from docutils import nodes from docutils.parsers.rst import Directive from docutils.parsers.rst import directives import os import re from hashlib import sha1 import sphinx from sphinx.builders import Builder # note: Really a workaround for some internal Sphinx changes. # See VisualDirective.name_source_snippet for details if sphinx.version_info < (1, 5, 0): from sphinx.ext.autodoc import AutodocReporter as ReporterInQuestion else: from sphinx.util.docutils import LoggingReporter as ReporterInQuestion try: from gen_example import render_snippet except ImportError as error: render_snippet = None print ( "Could not import snippet renderer. " "Will use static resources. Import error: %s" % error ) VISUAL_EXAMPLES_DIR = "visual_examples" # todo: maybe should be more generic from sphinx conf SOURCE_DIR = os.path.join(os.path.dirname(__file__)) def flag(argument): """Reimplement directives.flag to return True instead of None Check for a valid flag option (no argument) and return ``None``. (Directive option conversion function.) Raise ``ValueError`` if an argument is found. """ if argument and argument.strip(): raise ValueError('no argument is allowed; "%s" supplied' % argument) else: return True def nonnegative_int_list(argument): if ',' in argument: entries = argument.split(',') else: entries = argument.split() return [directives.nonnegative_int(entry) for entry in entries] def click_list(argument): value = nonnegative_int_list(argument) if len(value) != 2: ValueError("argument must contain 3 non-negative values") return value class WrapsDirective(Directive): has_content = True def run(self): head = nodes.paragraph() head.append(nodes.inline("Wraps API:", "Wraps API: ")) source = '\n'.join(self.content.data) literal_node = nodes.literal_block(source, source) literal_node['laguage'] = 'C++' return [head, literal_node] class VisualDirective(Directive): has_content = True final_argument_whitespace = True option_spec = { 'title': directives.unchanged, 'introduction': directives.unchanged, 'inter': directives.unchanged, 'width': directives.positive_int, 'height': directives.positive_int, 'auto_layout': flag, 'click': click_list, } def run(self): source = '\n'.join(self.content.data) literal = nodes.literal_block(source, source) literal['visualnodetype'] = True literal['language'] = 'python' # docutils document model is insane! head1 = nodes.paragraph() introduction = self.options.pop('introduction', "Example:") head1.append(nodes.inline(introduction, introduction)) inter = self.options.pop('inter', "Outputs:") head2 = nodes.paragraph() head2.append( nodes.section("foo", nodes.inline(inter, inter)) ) directive_nodes = [ head1, literal, head2, self.get_image_node(source) ] return directive_nodes def name_source_snippet(self, source): env = self.state.document.settings.env if ( # note: This is series of hacks due to internal Sphinx changes # In Sphinx==1.4.8 it was enough to check against # AutodocReporter. Now it become really complicated. # We should redo this in future if we will have more # similar problems isinstance(self.state.reporter, ReporterInQuestion) and self.state.parent and self.state.parent.parent and self.state.parent.parent.children[0]['names'] ): # If it is generated by autodoc then autogenerate title from # the function/method/class signature # note: hacky assumption that this is a signature node signature_node = self.state.parent.parent.children[0] signature = signature_node['names'][0] occurence = env.new_serialno(signature) name = signature + '_' + str(occurence) else: # If we could not quess then use explicit title or hexdigest name = self.options.get('title', sha1(source.encode()).hexdigest()) return self.phrase_to_filename(name) def phrase_to_filename(self, phrase): """Convert phrase to normilized file name.""" # remove non-word characters name = re.sub(r"[^\w\s\.]", '', phrase.strip().lower()) # replace whitespace with underscores name = re.sub(r"\s+", '_', name) return name + '.png' def get_image_node(self, source): file_name = self.name_source_snippet(source) file_path = os.path.join(VISUAL_EXAMPLES_DIR, file_name) env = self.state.document.settings.env if all([ render_snippet, env.config['render_examples'], not os.environ.get('SPHINX_DISABLE_RENDER', False), ]): try: render_snippet( source, file_path, output_dir=SOURCE_DIR, **self.options ) except: print("problematic code:\n%s" % source) raise img = nodes.image() img['uri'] = "/" + file_path return img class VisualBuilder(Builder): """ Collects visual examples in the documentation for testing purpose. """ name = 'vistest' def get_outdated_docs(self): return self.env.found_docs def write(self, build_docnames, updated_docnames, method='update'): # todo: monkey patching, rewrite self.snippets = [] if build_docnames is None: build_docnames = sorted(self.env.all_docs) for docname in build_docnames: # no need to resolve the doctree doctree = self.env.get_doctree(docname) self.snippets.extend(self.collect_doc(docname, doctree)) @staticmethod def traverse_condition(node): return isinstance( node, nodes.literal_block ) and 'visualnodetype' in node def collect_doc(self, docname, doctree): return [ (node.source, node.astext()) for node in doctree.traverse(self.traverse_condition) ] def setup(app): app.add_config_value('render_examples', False, 'html') app.add_directive('wraps', WrapsDirective) app.add_directive('visual-example', VisualDirective) app.add_builder(VisualBuilder) return {'version': '0.1'}