diff --git a/CHANGES b/CHANGES index 970422b3b4..891df66bb6 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,7 @@ Upcoming Release ===================== +* ENH: Sphinx extension to plot workflows (https://github.com/nipy/nipype/pull/1896) * ENH: Added non-steady state detector for EPI data (https://github.com/nipy/nipype/pull/1839) * ENH: Enable new BBRegister init options for FSv6+ (https://github.com/nipy/nipype/pull/1811) * REF: Splits nipype.interfaces.utility into base, csv, and wrappers (https://github.com/nipy/nipype/pull/1828) @@ -22,7 +23,7 @@ Upcoming Release 0.13.0-rc1 (January 4, 2017) =============================== - + * FIX: Compatibility with traits 4.6 (https://github.com/nipy/nipype/pull/1770) * FIX: Multiproc deadlock (https://github.com/nipy/nipype/pull/1756) * TST: Replace nose and unittest with pytest (https://github.com/nipy/nipype/pull/1722, https://github.com/nipy/nipype/pull/1751) diff --git a/doc/conf.py b/doc/conf.py index 9ed5c87da9..116ff3f36e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -42,16 +42,16 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.todo', - 'sphinx.ext.pngmath', + 'sphinx.ext.imgmath', 'sphinx.ext.inheritance_diagram', 'sphinx.ext.graphviz', 'sphinx.ext.autodoc', 'sphinx.ext.doctest', - 'sphinx.ext.pngmath', 'sphinx.ext.autosummary', 'numpy_ext.numpydoc', 'matplotlib.sphinxext.plot_directive', 'matplotlib.sphinxext.only_directives', + 'nipype.sphinxext.plot_workflow', #'IPython.sphinxext.ipython_directive', #'IPython.sphinxext.ipython_console_highlighting' ] diff --git a/doc/documentation.rst b/doc/documentation.rst index 8ca2e503c8..3468525492 100644 --- a/doc/documentation.rst +++ b/doc/documentation.rst @@ -24,10 +24,12 @@ Previous versions: `0.12.0 `_ `0.11.0 :maxdepth: 2 users/index + .. toctree:: :maxdepth: 1 changes + * Developer .. toctree:: diff --git a/doc/users/index.rst b/doc/users/index.rst index c9cbcd961b..3c39ce08b2 100644 --- a/doc/users/index.rst +++ b/doc/users/index.rst @@ -39,6 +39,8 @@ mipav nipypecmd aws + resource_sched_profiler + sphinx_ext diff --git a/doc/users/sphinx_ext.rst b/doc/users/sphinx_ext.rst new file mode 100644 index 0000000000..6326a6041a --- /dev/null +++ b/doc/users/sphinx_ext.rst @@ -0,0 +1,14 @@ + +.. _sphinx_ext: + +Sphinx extensions +----------------- + + +To help users document their *Nipype*-based code, the software is shipped +with a set of extensions (currently only one) to customize the appearance +and simplify the generation process. + +.. automodule:: nipype.sphinxext.plot_workflow + :undoc-members: + :noindex: \ No newline at end of file diff --git a/nipype/info.py b/nipype/info.py index 81a8adce0a..c70b555ee3 100644 --- a/nipype/info.py +++ b/nipype/info.py @@ -152,7 +152,7 @@ def get_nipype_gitversion(): ] EXTRA_REQUIRES = { - 'doc': ['Sphinx>=0.3', 'matplotlib', 'pydotplus'], + 'doc': ['Sphinx>=1.4', 'matplotlib', 'pydotplus'], 'tests': TESTS_REQUIRES, 'fmri': ['nitime', 'nilearn', 'dipy', 'nipy', 'matplotlib'], 'profiler': ['psutil'], diff --git a/nipype/pipeline/engine/utils.py b/nipype/pipeline/engine/utils.py index ce2f927b15..0211de16f6 100644 --- a/nipype/pipeline/engine/utils.py +++ b/nipype/pipeline/engine/utils.py @@ -1018,20 +1018,18 @@ def export_graph(graph_in, base_dir=None, show=False, use_execgraph=False, suffix='_detailed.dot', use_ext=False, newpath=base_dir) - logger.info('Creating detailed dot file: %s' % outfname) _write_detailed_dot(graph, outfname) cmd = 'dot -T%s -O %s' % (format, outfname) res = CommandLine(cmd, terminal_output='allatonce').run() if res.runtime.returncode: logger.warn('dot2png: %s', res.runtime.stderr) pklgraph = _create_dot_graph(graph, show_connectinfo, simple_form) - outfname = fname_presuffix(dotfilename, - suffix='.dot', - use_ext=False, - newpath=base_dir) - nx.drawing.nx_pydot.write_dot(pklgraph, outfname) - logger.info('Creating dot file: %s' % outfname) - cmd = 'dot -T%s -O %s' % (format, outfname) + simplefname = fname_presuffix(dotfilename, + suffix='.dot', + use_ext=False, + newpath=base_dir) + nx.drawing.nx_pydot.write_dot(pklgraph, simplefname) + cmd = 'dot -T%s -O %s' % (format, simplefname) res = CommandLine(cmd, terminal_output='allatonce').run() if res.runtime.returncode: logger.warn('dot2png: %s', res.runtime.stderr) @@ -1041,6 +1039,10 @@ def export_graph(graph_in, base_dir=None, show=False, use_execgraph=False, if show_connectinfo: nx.draw_networkx_edge_labels(pklgraph, pos) + if format != 'dot': + outfname += '.%s' % format + return outfname + def format_dot(dotfilename, format=None): """Dump a directed graph (Linux only; install via `brew` on OSX)""" @@ -1052,8 +1054,10 @@ def format_dot(dotfilename, format=None): raise IOError("Cannot draw directed graph; executable 'dot' is unavailable") else: raise ioe - else: - logger.info('Converting dotfile: %s to %s format' % (dotfilename, format)) + + if format != 'dot': + dotfilename += '.%s' % format + return dotfilename def make_output_dir(outdir): diff --git a/nipype/pipeline/engine/workflows.py b/nipype/pipeline/engine/workflows.py index 5a8c5eed56..50b47c63a9 100644 --- a/nipype/pipeline/engine/workflows.py +++ b/nipype/pipeline/engine/workflows.py @@ -429,15 +429,19 @@ def write_graph(self, dotfilename='graph.dot', graph2use='hierarchical', self.write_hierarchical_dotfile(dotfilename=dotfilename, colored=graph2use == "colored", simple_form=simple_form) - format_dot(dotfilename, format=format) + outfname = format_dot(dotfilename, format=format) else: graph = self._graph if graph2use in ['flat', 'exec']: graph = self._create_flat_graph() if graph2use == 'exec': graph = generate_expanded_graph(deepcopy(graph)) - export_graph(graph, base_dir, dotfilename=dotfilename, - format=format, simple_form=simple_form) + outfname = export_graph(graph, base_dir, dotfilename=dotfilename, + format=format, simple_form=simple_form) + + logger.info('Generated workflow graph: %s (graph2use=%s, simple_form=%s).' % ( + outfname, graph2use, simple_form)) + return outfname def write_hierarchical_dotfile(self, dotfilename=None, colored=False, simple_form=True): diff --git a/nipype/sphinxext/__init__.py b/nipype/sphinxext/__init__.py new file mode 100644 index 0000000000..b2033960f3 --- /dev/null +++ b/nipype/sphinxext/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +from __future__ import print_function, division, absolute_import, unicode_literals diff --git a/nipype/sphinxext/plot_workflow.py b/nipype/sphinxext/plot_workflow.py new file mode 100644 index 0000000000..1ed10aef42 --- /dev/null +++ b/nipype/sphinxext/plot_workflow.py @@ -0,0 +1,720 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +""" +:mod:`nipype.sphinxext.plot_workflow` -- Workflow plotting extension +==================================================================== + + +A directive for including a nipype workflow graph in a Sphinx document. + +This code is forked from the plot_figure sphinx extension of matplotlib. + +By default, in HTML output, `workflow` will include a .png file with a +link to a high-res .png. In LaTeX output, it will include a +.pdf. +The source code for the workflow may be included as **inline content** to +the directive `workflow`:: + + .. workflow :: + :graph2use: flat + :simple_form: no + + from nipype.workflows.dmri.camino.connectivity_mapping import create_connectivity_pipeline + wf = create_connectivity_pipeline() + + +For example, the following graph has been generated inserting the previous +code block in this documentation: + +.. workflow :: + :graph2use: flat + :simple_form: no + + from nipype.workflows.dmri.camino.connectivity_mapping import create_connectivity_pipeline + wf = create_connectivity_pipeline() + + +Options +------- + +The ``workflow`` directive supports the following options: + graph2use : {'hierarchical', 'colored', 'flat', 'orig', 'exec'} + Specify the type of graph to be generated. + simple_form: bool + Whether the graph will be in detailed or simple form. + format : {'python', 'doctest'} + Specify the format of the input + include-source : bool + Whether to display the source code. The default can be changed + using the `workflow_include_source` variable in conf.py + encoding : str + If this source file is in a non-UTF8 or non-ASCII encoding, + the encoding must be specified using the `:encoding:` option. + The encoding will not be inferred using the ``-*- coding -*-`` + metacomment. + +Additionally, this directive supports all of the options of the +`image` directive, except for `target` (since workflow will add its own +target). These include `alt`, `height`, `width`, `scale`, `align` and +`class`. + +Configuration options +--------------------- + +The workflow directive has the following configuration options: + graph2use + Select a graph type to use + simple_form + determines if the node name shown in the visualization is either of the form nodename + (package) when set to True or nodename.Class.package when set to False. + wf_include_source + Default value for the include-source option + wf_html_show_source_link + Whether to show a link to the source in HTML. + wf_pre_code + Code that should be executed before each workflow. + wf_basedir + Base directory, to which ``workflow::`` file names are relative + to. (If None or empty, file names are relative to the + directory where the file containing the directive is.) + wf_formats + File formats to generate. List of tuples or strings:: + [(suffix, dpi), suffix, ...] + that determine the file format and the DPI. For entries whose + DPI was omitted, sensible defaults are chosen. When passing from + the command line through sphinx_build the list should be passed as + suffix:dpi,suffix:dpi, .... + wf_html_show_formats + Whether to show links to the files in HTML. + wf_rcparams + A dictionary containing any non-standard rcParams that should + be applied before each workflow. + wf_apply_rcparams + By default, rcParams are applied when `context` option is not used in + a workflow directive. This configuration option overrides this behavior + and applies rcParams before each workflow. + wf_working_directory + By default, the working directory will be changed to the directory of + the example, so the code can get at its data files, if any. Also its + path will be added to `sys.path` so it can import any helper modules + sitting beside it. This configuration option can be used to specify + a central directory (also added to `sys.path`) where data files and + helper modules for all code are located. + wf_template + Provide a customized template for preparing restructured text. + +""" +from __future__ import print_function, division, absolute_import, unicode_literals + +import sys, os, shutil, io, re, textwrap +from os.path import relpath +from errno import EEXIST +import traceback + +from docutils.parsers.rst import directives +from docutils.parsers.rst.directives.images import Image + + +try: + # Sphinx depends on either Jinja or Jinja2 + import jinja2 + def format_template(template, **kw): + return jinja2.Template(template).render(**kw) +except ImportError: + import jinja + def format_template(template, **kw): + return jinja.from_string(template, **kw) + +from builtins import str, bytes +align = Image.align + +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + +def _mkdirp(folder): + """ + Equivalent to bash's mkdir -p + """ + if sys.version_info > (3, 4, 1): + os.makedirs(folder, exist_ok=True) + return folder + + try: + os.makedirs(folder) + except OSError as exc: + if exc.errno != EEXIST or not os.path.isdir(folder): + raise + + return folder + + +def wf_directive(name, arguments, options, content, lineno, + content_offset, block_text, state, state_machine): + return run(arguments, content, options, state_machine, state, lineno) +wf_directive.__doc__ = __doc__ + +def _option_boolean(arg): + if not arg or not arg.strip(): + # no argument given, assume used as a flag + return True + elif arg.strip().lower() in ('no', '0', 'false'): + return False + elif arg.strip().lower() in ('yes', '1', 'true'): + return True + else: + raise ValueError('"%s" unknown boolean' % arg) + +def _option_graph2use(arg): + return directives.choice(arg, ('hierarchical', 'colored', 'flat', 'orig', 'exec')) + +def _option_context(arg): + if arg in [None, 'reset', 'close-figs']: + return arg + raise ValueError("argument should be None or 'reset' or 'close-figs'") + + +def _option_format(arg): + return directives.choice(arg, ('python', 'doctest')) + + +def _option_align(arg): + return directives.choice(arg, ("top", "middle", "bottom", "left", "center", + "right")) + + +def mark_wf_labels(app, document): + """ + To make graphs referenceable, we need to move the reference from + the "htmlonly" (or "latexonly") node to the actual figure node + itself. + """ + for name, explicit in list(document.nametypes.items()): + if not explicit: + continue + labelid = document.nameids[name] + if labelid is None: + continue + node = document.ids[labelid] + if node.tagname in ('html_only', 'latex_only'): + for n in node: + if n.tagname == 'figure': + sectname = name + for c in n: + if c.tagname == 'caption': + sectname = c.astext() + break + + node['ids'].remove(labelid) + node['names'].remove(name) + n['ids'].append(labelid) + n['names'].append(name) + document.settings.env.labels[name] = \ + document.settings.env.docname, labelid, sectname + break + + +def setup(app): + setup.app = app + setup.config = app.config + setup.confdir = app.confdir + + options = {'alt': directives.unchanged, + 'height': directives.length_or_unitless, + 'width': directives.length_or_percentage_or_unitless, + 'scale': directives.nonnegative_int, + 'align': _option_align, + 'class': directives.class_option, + 'include-source': _option_boolean, + 'format': _option_format, + 'context': _option_context, + 'nofigs': directives.flag, + 'encoding': directives.encoding, + 'graph2use': _option_graph2use, + 'simple_form': _option_boolean + } + + app.add_directive('workflow', wf_directive, True, (0, 2, False), **options) + app.add_config_value('graph2use', 'hierarchical', 'html') + app.add_config_value('simple_form', True, 'html') + app.add_config_value('wf_pre_code', None, True) + app.add_config_value('wf_include_source', False, True) + app.add_config_value('wf_html_show_source_link', True, True) + app.add_config_value('wf_formats', ['png', 'svg', 'pdf'], True) + app.add_config_value('wf_basedir', None, True) + app.add_config_value('wf_html_show_formats', True, True) + app.add_config_value('wf_rcparams', {}, True) + app.add_config_value('wf_apply_rcparams', False, True) + app.add_config_value('wf_working_directory', None, True) + app.add_config_value('wf_template', None, True) + + app.connect('doctree-read'.encode() if PY2 else 'doctree-read', mark_wf_labels) + + metadata = {'parallel_read_safe': True, 'parallel_write_safe': True} + return metadata + + +#------------------------------------------------------------------------------ +# Doctest handling +#------------------------------------------------------------------------------ + +def contains_doctest(text): + try: + # check if it's valid Python as-is + compile(text, '', 'exec') + return False + except SyntaxError: + pass + r = re.compile(r'^\s*>>>', re.M) + m = r.search(text) + return bool(m) + + +def unescape_doctest(text): + """ + Extract code from a piece of text, which contains either Python code + or doctests. + """ + if not contains_doctest(text): + return text + + code = "" + for line in text.split("\n"): + m = re.match(r'^\s*(>>>|\.\.\.) (.*)$', line) + if m: + code += m.group(2) + "\n" + elif line.strip(): + code += "# " + line.strip() + "\n" + else: + code += "\n" + return code + + +def remove_coding(text): + """ + Remove the coding comment, which exec doesn't like. + """ + sub_re = re.compile("^#\s*-\*-\s*coding:\s*.*-\*-$", flags=re.MULTILINE) + return sub_re.sub("", text) + +#------------------------------------------------------------------------------ +# Template +#------------------------------------------------------------------------------ + + +TEMPLATE = """ +{{ source_code }} +{{ only_html }} + {% for img in images %} + .. figure:: {{ build_dir }}/{{ img.basename }}.{{ default_fmt }} + {% for option in options -%} + {{ option }} + {% endfor %} + {% if html_show_formats and multi_image -%} + ( + {%- for fmt in img.formats -%} + {%- if not loop.first -%}, {% endif -%} + `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ + {%- endfor -%} + ) + {%- endif -%} + {{ caption }} + {% endfor %} + {% if source_link or (html_show_formats and not multi_image) %} + ( + {%- if source_link -%} + `Source code <{{ source_link }}>`__ + {%- endif -%} + {%- if html_show_formats and not multi_image -%} + {%- for img in images -%} + {%- for fmt in img.formats -%} + {%- if source_link or not loop.first -%}, {% endif -%} + `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ + {%- endfor -%} + {%- endfor -%} + {%- endif -%} + ) + {% endif %} +{{ only_latex }} + {% for img in images %} + {% if 'pdf' in img.formats -%} + .. figure:: {{ build_dir }}/{{ img.basename }}.pdf + {% for option in options -%} + {{ option }} + {% endfor %} + {{ caption }} + {% endif -%} + {% endfor %} +{{ only_texinfo }} + {% for img in images %} + .. image:: {{ build_dir }}/{{ img.basename }}.png + {% for option in options -%} + {{ option }} + {% endfor %} + {% endfor %} +""" + +exception_template = """ +.. htmlonly:: + [`source code <%(linkdir)s/%(basename)s.py>`__] +Exception occurred rendering plot. +""" + +# the context of the plot for all directives specified with the +# :context: option +wf_context = dict() + +class ImageFile(object): + def __init__(self, basename, dirname): + self.basename = basename + self.dirname = dirname + self.formats = [] + + def filename(self, fmt): + return os.path.join(self.dirname, "%s.%s" % (self.basename, fmt)) + + def filenames(self): + return [self.filename(fmt) for fmt in self.formats] + + +def out_of_date(original, derived): + """ + Returns True if derivative is out-of-date wrt original, + both of which are full file paths. + """ + return (not os.path.exists(derived) or + (os.path.exists(original) and + os.stat(derived).st_mtime < os.stat(original).st_mtime)) + + +class GraphError(RuntimeError): + pass + + +def run_code(code, code_path, ns=None, function_name=None): + """ + Import a Python module from a path, and run the function given by + name, if function_name is not None. + """ + + # Change the working directory to the directory of the example, so + # it can get at its data files, if any. Add its path to sys.path + # so it can import any helper modules sitting beside it. + pwd = str(os.getcwd()) + old_sys_path = list(sys.path) + if setup.config.wf_working_directory is not None: + try: + os.chdir(setup.config.wf_working_directory) + except OSError as err: + raise OSError(str(err) + '\n`wf_working_directory` option in' + 'Sphinx configuration file must be a valid ' + 'directory path') + except TypeError as err: + raise TypeError(str(err) + '\n`wf_working_directory` option in ' + 'Sphinx configuration file must be a string or ' + 'None') + sys.path.insert(0, setup.config.wf_working_directory) + elif code_path is not None: + dirname = os.path.abspath(os.path.dirname(code_path)) + os.chdir(dirname) + sys.path.insert(0, dirname) + + # Reset sys.argv + old_sys_argv = sys.argv + sys.argv = [code_path] + + # Redirect stdout + stdout = sys.stdout + if PY3: + sys.stdout = io.StringIO() + else: + from cStringIO import StringIO + sys.stdout = StringIO() + + # Assign a do-nothing print function to the namespace. There + # doesn't seem to be any other way to provide a way to (not) print + # that works correctly across Python 2 and 3. + def _dummy_print(*arg, **kwarg): + pass + + try: + try: + code = unescape_doctest(code) + if ns is None: + ns = {} + if not ns: + if setup.config.wf_pre_code is not None: + exec(str(setup.config.wf_pre_code), ns) + ns['print'] = _dummy_print + if "__main__" in code: + exec("__name__ = '__main__'", ns) + code = remove_coding(code) + exec(code, ns) + if function_name is not None: + exec(function_name + "()", ns) + except (Exception, SystemExit) as err: + raise GraphError(traceback.format_exc()) + finally: + os.chdir(pwd) + sys.argv = old_sys_argv + sys.path[:] = old_sys_path + sys.stdout = stdout + return ns + +def get_wf_formats(config): + default_dpi = {'png': 80, 'hires.png': 200, 'pdf': 200} + formats = [] + wf_formats = config.wf_formats + if isinstance(wf_formats, (str, bytes)): + # String Sphinx < 1.3, Split on , to mimic + # Sphinx 1.3 and later. Sphinx 1.3 always + # returns a list. + wf_formats = wf_formats.split(',') + for fmt in wf_formats: + if isinstance(fmt, (str, bytes)): + if ':' in fmt: + suffix, dpi = fmt.split(':') + formats.append((str(suffix), int(dpi))) + else: + formats.append((fmt, default_dpi.get(fmt, 80))) + elif isinstance(fmt, (tuple, list)) and len(fmt) == 2: + formats.append((str(fmt[0]), int(fmt[1]))) + else: + raise GraphError('invalid image format "%r" in wf_formats' % fmt) + return formats + + +def render_figures(code, code_path, output_dir, output_base, context, + function_name, config, graph2use, simple_form, + context_reset=False, close_figs=False): + """ + Run a nipype workflow creation script and save the graph in *output_dir*. + Save the images under *output_dir* with file names derived from + *output_base* + """ + formats = get_wf_formats(config) + ns = wf_context if context else {} + if context_reset: + wf_context.clear() + + run_code(code, code_path, ns, function_name) + img = ImageFile(output_base, output_dir) + + for fmt, dpi in formats: + try: + img_path = img.filename(fmt) + imgname, ext = os.path.splitext(os.path.basename(img_path)) + ns['wf'].base_dir = output_dir + src = ns['wf'].write_graph(imgname, format=ext[1:], + graph2use=graph2use, + simple_form=simple_form) + shutil.move(src, img_path) + except Exception as err: + raise GraphError(traceback.format_exc()) + + img.formats.append(fmt) + + return [(code, [img])] + + +def run(arguments, content, options, state_machine, state, lineno): + document = state_machine.document + config = document.settings.env.config + nofigs = 'nofigs' in options + + formats = get_wf_formats(config) + default_fmt = formats[0][0] + + graph2use = options.get('graph2use', 'hierarchical') + simple_form = options.get('simple_form', True) + + options.setdefault('include-source', config.wf_include_source) + keep_context = 'context' in options + context_opt = None if not keep_context else options['context'] + + rst_file = document.attributes['source'] + rst_dir = os.path.dirname(rst_file) + + if len(arguments): + if not config.wf_basedir: + source_file_name = os.path.join(setup.app.builder.srcdir, + directives.uri(arguments[0])) + else: + source_file_name = os.path.join(setup.confdir, config.wf_basedir, + directives.uri(arguments[0])) + + # If there is content, it will be passed as a caption. + caption = '\n'.join(content) + + # If the optional function name is provided, use it + if len(arguments) == 2: + function_name = arguments[1] + else: + function_name = None + + with io.open(source_file_name, 'r', encoding='utf-8') as fd: + code = fd.read() + output_base = os.path.basename(source_file_name) + else: + source_file_name = rst_file + code = textwrap.dedent("\n".join([str(c) for c in content])) + counter = document.attributes.get('_wf_counter', 0) + 1 + document.attributes['_wf_counter'] = counter + base, _ = os.path.splitext(os.path.basename(source_file_name)) + output_base = '%s-%d.py' % (base, counter) + function_name = None + caption = '' + + base, source_ext = os.path.splitext(output_base) + if source_ext in ('.py', '.rst', '.txt'): + output_base = base + else: + source_ext = '' + + # ensure that LaTeX includegraphics doesn't choke in foo.bar.pdf filenames + output_base = output_base.replace('.', '-') + + # is it in doctest format? + is_doctest = contains_doctest(code) + if 'format' in options: + if options['format'] == 'python': + is_doctest = False + else: + is_doctest = True + + # determine output directory name fragment + source_rel_name = relpath(source_file_name, setup.confdir) + source_rel_dir = os.path.dirname(source_rel_name) + while source_rel_dir.startswith(os.path.sep): + source_rel_dir = source_rel_dir[1:] + + # build_dir: where to place output files (temporarily) + build_dir = os.path.join(os.path.dirname(setup.app.doctreedir), + 'wf_directive', + source_rel_dir) + # get rid of .. in paths, also changes pathsep + # see note in Python docs for warning about symbolic links on Windows. + # need to compare source and dest paths at end + build_dir = os.path.normpath(build_dir) + + if not os.path.exists(build_dir): + os.makedirs(build_dir) + + # output_dir: final location in the builder's directory + dest_dir = os.path.abspath(os.path.join(setup.app.builder.outdir, + source_rel_dir)) + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) # no problem here for me, but just use built-ins + + # how to link to files from the RST file + dest_dir_link = os.path.join(relpath(setup.confdir, rst_dir), + source_rel_dir).replace(os.path.sep, '/') + try: + build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, '/') + except ValueError: + # on Windows, relpath raises ValueError when path and start are on + # different mounts/drives + build_dir_link = build_dir + source_link = dest_dir_link + '/' + output_base + source_ext + + # make figures + try: + results = render_figures(code, + source_file_name, + build_dir, + output_base, + keep_context, + function_name, + config, + graph2use, + simple_form, + context_reset=context_opt == 'reset', + close_figs=context_opt == 'close-figs') + errors = [] + except GraphError as err: + reporter = state.memo.reporter + sm = reporter.system_message( + 2, "Exception occurred in plotting %s\n from %s:\n%s" % (output_base, + source_file_name, err), + line=lineno) + results = [(code, [])] + errors = [sm] + + # Properly indent the caption + caption = '\n'.join(' ' + line.strip() + for line in caption.split('\n')) + + # generate output restructuredtext + total_lines = [] + for j, (code_piece, images) in enumerate(results): + if options['include-source']: + if is_doctest: + lines = [''] + lines += [row.rstrip() for row in code_piece.split('\n')] + else: + lines = ['.. code-block:: python', ''] + lines += [' %s' % row.rstrip() + for row in code_piece.split('\n')] + source_code = "\n".join(lines) + else: + source_code = "" + + if nofigs: + images = [] + + opts = [':%s: %s' % (key, val) for key, val in list(options.items()) + if key in ('alt', 'height', 'width', 'scale', 'align', 'class')] + + only_html = ".. only:: html" + only_latex = ".. only:: latex" + only_texinfo = ".. only:: texinfo" + + # Not-None src_link signals the need for a source link in the generated + # html + if j == 0 and config.wf_html_show_source_link: + src_link = source_link + else: + src_link = None + + result = format_template( + config.wf_template or TEMPLATE, + default_fmt=default_fmt, + dest_dir=dest_dir_link, + build_dir=build_dir_link, + source_link=src_link, + multi_image=len(images) > 1, + only_html=only_html, + only_latex=only_latex, + only_texinfo=only_texinfo, + options=opts, + images=images, + source_code=source_code, + html_show_formats=config.wf_html_show_formats and len(images), + caption=caption) + + total_lines.extend(result.split("\n")) + total_lines.extend("\n") + + if total_lines: + state_machine.insert_input(total_lines, source=source_file_name) + + # copy image files to builder's output directory, if necessary + _mkdirp(dest_dir) + for code_piece, images in results: + for img in images: + for fn in img.filenames(): + destimg = os.path.join(dest_dir, os.path.basename(fn)) + if fn != destimg: + shutil.copyfile(fn, destimg) + + # copy script (if necessary) + target_name = os.path.join(dest_dir, output_base + source_ext) + with io.open(target_name, 'w', encoding="utf-8") as f: + if source_file_name == rst_file: + code_escaped = unescape_doctest(code) + else: + code_escaped = code + f.write(code_escaped) + + return errors