Repository: nipy/nipype

Issue:
Sphinx 3 issues
Tracking thread for Sphinx 3 upgrade.

* [ ] `plot_workflow` directive is failing:
   ```
    Creating file /tmp/src/nipype/doc/users/examples/smoothing_comparison.rst.

    Exception occurred:
      File "/usr/local/lib/python3.7/site-packages/nipype/sphinxext/plot_workflow.py", line 256, in setup
        app.add_directive("workflow", wf_directive, True, (0, 2, False), **options)
    TypeError: add_directive() got an unexpected keyword argument 'alt'
   ```


Discussion:

Resolving patch:
diff --git a/doc/conf.py b/doc/conf.py
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -25,7 +25,7 @@
 
 conf_py = Path(__file__)
 
-example_dir = conf_py.parent / 'users' / 'examples'
+example_dir = conf_py.parent / "users" / "examples"
 shutil.rmtree(example_dir, ignore_errors=True)
 example_dir.mkdir(parents=True)
 python_dir = conf_py.parent / "_static" / "python"
@@ -34,18 +34,46 @@
 ex2rst = str(conf_py.parent.parent / "tools" / "ex2rst")
 
 with TemporaryDirectory() as tmpdir:
-    sp.run(["git", "clone", "--depth", "1", "https://github.com/niflows/nipype1-examples.git",
-            tmpdir], check=True)
+    sp.run(
+        [
+            "git",
+            "clone",
+            "--depth",
+            "1",
+            "https://github.com/niflows/nipype1-examples.git",
+            tmpdir,
+        ],
+        check=True,
+    )
     source_dir = Path(tmpdir) / "package" / "niflow" / "nipype1" / "examples"
     shutil.copytree(source_dir, python_dir)
 
-sp.run(["python", ex2rst, "--outdir", str(example_dir), str(python_dir),
-        "-x", str(python_dir / "test_spm.py"),
-        "-x", str(python_dir / "__init__.py"),
-        "-x", str(python_dir / "cli.py")],
-       check=True)
-sp.run(["python", ex2rst, "--outdir", str(example_dir), str(python_dir / "frontiers_paper")],
-       check=True)
+sp.run(
+    [
+        "python",
+        ex2rst,
+        "--outdir",
+        str(example_dir),
+        str(python_dir),
+        "-x",
+        str(python_dir / "test_spm.py"),
+        "-x",
+        str(python_dir / "__init__.py"),
+        "-x",
+        str(python_dir / "cli.py"),
+    ],
+    check=True,
+)
+sp.run(
+    [
+        "python",
+        ex2rst,
+        "--outdir",
+        str(example_dir),
+        str(python_dir / "frontiers_paper"),
+    ],
+    check=True,
+)
 
 
 # If extensions (or modules to document with autodoc) are in another directory,
@@ -58,33 +86,33 @@
 # 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.autodoc',
-    'sphinx.ext.doctest',
-    'sphinx.ext.graphviz',
-    'sphinx.ext.mathjax',
-    'sphinx.ext.inheritance_diagram',
-    'sphinx.ext.todo',
-    'sphinxcontrib.apidoc',
-    'matplotlib.sphinxext.plot_directive',
-    'nbsphinx',
-    'nipype.sphinxext.plot_workflow',
-    'nipype.sphinxext.apidoc',
-    'nipype.sphinxext.documenter',
+    "sphinx.ext.autodoc",
+    "sphinx.ext.doctest",
+    "sphinx.ext.graphviz",
+    "sphinx.ext.mathjax",
+    "sphinx.ext.inheritance_diagram",
+    "sphinx.ext.todo",
+    "sphinxcontrib.apidoc",
+    "matplotlib.sphinxext.plot_directive",
+    "nbsphinx",
+    "nipype.sphinxext.plot_workflow",
+    "nipype.sphinxext.apidoc",
+    "nipype.sphinxext.documenter",
 ]
 
 autodoc_mock_imports = [
-    'matplotlib',
-    'nilearn',
-    'nipy',
-    'nitime',
-    'numpy',
-    'pandas',
-    'seaborn',
-    'skimage',
-    'svgutils',
-    'transforms3d',
-    'tvtk',
-    'vtk'
+    "matplotlib",
+    "nilearn",
+    "nipy",
+    "nitime",
+    "numpy",
+    "pandas",
+    "seaborn",
+    "skimage",
+    "svgutils",
+    "transforms3d",
+    "tvtk",
+    "vtk",
 ]
 
 # Accept custom section names to be parsed for numpy-style docstrings
@@ -93,33 +121,33 @@
 # https://github.com/sphinx-contrib/napoleon/pull/10 is merged.
 napoleon_use_param = False
 napoleon_custom_sections = [
-    ('Inputs', 'Parameters'),
-    ('Outputs', 'Parameters'),
-    ('Attributes', 'Parameters'),
-    ('Mandatory Inputs', 'Parameters'),
-    ('Optional Inputs', 'Parameters'),
+    ("Inputs", "Parameters"),
+    ("Outputs", "Parameters"),
+    ("Attributes", "Parameters"),
+    ("Mandatory Inputs", "Parameters"),
+    ("Optional Inputs", "Parameters"),
 ]
 
 
-on_rtd = os.environ.get('READTHEDOCS') == 'True'
+on_rtd = os.environ.get("READTHEDOCS") == "True"
 if on_rtd:
-    extensions.append('readthedocs_ext.readthedocs')
+    extensions.append("readthedocs_ext.readthedocs")
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix of source filenames.
-source_suffix = '.rst'
+source_suffix = ".rst"
 
 # The encoding of source files.
-#source_encoding = 'utf-8'
+# source_encoding = 'utf-8'
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # General information about the project.
-project = u'nipype'
-copyright = u'2009-20, Neuroimaging in Python team'
+project = u"nipype"
+copyright = u"2009-20, Neuroimaging in Python team"
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
@@ -132,55 +160,55 @@
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
-#language = None
+# language = None
 
 # There are two options for replacing |today|: either, you set today to some
 # non-false value, then it is used:
-#today = ''
+# today = ''
 # Else, today_fmt is used as the format for a strftime call.
-today_fmt = '%B %d, %Y, %H:%M PDT'
+today_fmt = "%B %d, %Y, %H:%M PDT"
 
 # List of documents that shouldn't be included in the build.
-unused_docs = ['api/generated/gen']
+unused_docs = ["api/generated/gen"]
 
 # List of directories, relative to source directory, that shouldn't be searched
 # for source files.
-exclude_trees = ['_build']
+exclude_trees = ["_build"]
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = [
-    '_build', 'Thumbs.db', '.DS_Store',
-]
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
 
 # The reST default role (used for this markup: `text`) to use for all documents.
-#default_role = None
+# default_role = None
 
 # If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
+# add_function_parentheses = True
 
 # If true, the current module name will be prepended to all description
 # unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
 
 # If true, sectionauthor and moduleauthor directives will be shown in the
 # output. They are ignored by default.
-#show_authors = False
+# show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
 
 # A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
 
 # -- Sphinxext configuration ---------------------------------------------------
 
 # Set attributes for layout of inheritance diagrams
-inheritance_graph_attrs = dict(rankdir="LR", size='"6.0, 8.0"', fontsize=14,
-                               ratio='compress')
-inheritance_node_attrs = dict(shape='ellipse', fontsize=14, height=0.75,
-                              color='dodgerblue1', style='filled')
+inheritance_graph_attrs = dict(
+    rankdir="LR", size='"6.0, 8.0"', fontsize=14, ratio="compress"
+)
+inheritance_node_attrs = dict(
+    shape="ellipse", fontsize=14, height=0.75, color="dodgerblue1", style="filled"
+)
 
 # Flag to show todo items in rendered output
 todo_include_todos = True
@@ -189,70 +217,72 @@
 
 # The theme to use for HTML and HTML Help pages.  Major themes that come with
 # Sphinx are currently 'default' and 'sphinxdoc'.
-html_theme = 'sphinxdoc'
+html_theme = "sphinxdoc"
 
 # The style sheet to use for HTML and HTML Help pages. A file of that name
 # must exist either in Sphinx' static/ path, or in one of the custom paths
 # given in html_static_path.
-html_style = 'nipype.css'
+html_style = "nipype.css"
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
-#html_theme_options = {}
+# html_theme_options = {}
 
 # Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
+# html_theme_path = []
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
-html_title = 'nipy pipeline and interfaces package'
+html_title = "nipy pipeline and interfaces package"
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-#html_logo = None
+# html_logo = None
 
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large.
-#html_favicon = None
+# html_favicon = None
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+# html_last_updated_fmt = '%b %d, %Y'
 
 # Content template for the index page.
-html_index = 'index.html'
+html_index = "index.html"
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
 
 # Custom sidebar templates, maps document names to template names.
-html_sidebars = {'**': ['gse.html', 'localtoc.html', 'sidebar_versions.html', 'indexsidebar.html'],
-                 'searchresults': ['sidebar_versions.html', 'indexsidebar.html'],
-                 'version': []}
+html_sidebars = {
+    "**": ["gse.html", "localtoc.html", "sidebar_versions.html", "indexsidebar.html"],
+    "searchresults": ["sidebar_versions.html", "indexsidebar.html"],
+    "version": [],
+}
 
 # Additional templates that should be rendered to pages, maps page names to
 # template names.
-#html_additional_pages = {'index': 'index.html'}
+# html_additional_pages = {'index': 'index.html'}
 
 # If false, no module index is generated.
-#html_use_modindex = True
+# html_use_modindex = True
 
 # If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
 
 # If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
 
 # If true, links to the reST sources are added to the pages.
 html_show_sourcelink = False
@@ -260,72 +290,81 @@
 # If true, an OpenSearch description file will be output, and all pages will
 # contain a <link> tag referring to it.  The value of this option must be the
 # base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
 
 # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = ''
+# html_file_suffix = ''
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'nipypedoc'
+htmlhelp_basename = "nipypedoc"
 
 
 # -- Options for LaTeX output --------------------------------------------------
 
 # The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
+# latex_paper_size = 'letter'
 
 # The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
+# latex_font_size = '10pt'
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
 latex_documents = [
-    ('interfaces', 'interfaces.tex', 'Nipype Interfaces Documentation',
-     'Neuroimaging in Python team', 'manual'),
+    (
+        "interfaces",
+        "interfaces.tex",
+        "Nipype Interfaces Documentation",
+        "Neuroimaging in Python team",
+        "manual",
+    ),
     # ('developers', 'developers.tex', 'Nipype API',
     #  'Neuroimaging in Python team', 'manual'),
-    ('examples', 'examples.tex', 'Nipype Examples',
-     'Neuroimaging in Python team', 'manual'),
+    (
+        "examples",
+        "examples.tex",
+        "Nipype Examples",
+        "Neuroimaging in Python team",
+        "manual",
+    ),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
 # the title page.
-#latex_logo = None
+# latex_logo = None
 
 # For "manual" documents, if this is true, then toplevel headings are parts,
 # not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
 
 # Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
+# latex_preamble = ''
 
 # Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
 
 # If false, no module index is generated.
-#latex_use_modindex = True
+# latex_use_modindex = True
 
 # -- apidoc extension configuration ------------------------------------------
-apidoc_module_dir = '../nipype'
-apidoc_output_dir = 'api/generated'
+apidoc_module_dir = "../nipype"
+apidoc_output_dir = "api/generated"
 apidoc_excluded_paths = [
-    '*/tests/*', 'tests/*',
-    'external/*',
-    'fixes/*',
-    'scripts/*',
-    'sphinxext/*',
-    'testing/*',
-    'workflows/*',
-    'conftest.py',
-    'info.py',
-    'pkg_info.py',
-    'refs.py',
+    "*/tests/*",
+    "tests/*",
+    "external/*",
+    "fixes/*",
+    "scripts/*",
+    "testing/*",
+    "workflows/*",
+    "conftest.py",
+    "info.py",
+    "pkg_info.py",
+    "refs.py",
 ]
 apidoc_separate_modules = True
-apidoc_extra_args = ['--module-first', '-d 1', '-T']
+apidoc_extra_args = ["--module-first", "-d 1", "-T"]
 
 # -- Options for intersphinx extension ---------------------------------------
 
 # Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'http://docs.python.org/': None}
-
+intersphinx_mapping = {"http://docs.python.org/": None}
diff --git a/nipype/sphinxext/plot_workflow.py b/nipype/sphinxext/plot_workflow.py
--- a/nipype/sphinxext/plot_workflow.py
+++ b/nipype/sphinxext/plot_workflow.py
@@ -118,7 +118,7 @@
 
 missing_imports = []
 try:
-    from docutils.parsers.rst import directives
+    from docutils.parsers.rst import directives, Directive
     from docutils.parsers.rst.directives.images import Image
 
     align = Image.align
@@ -146,26 +146,6 @@ def format_template(template, **kw):
         missing_imports.append(str(e))
 
 
-def wf_directive(
-    name,
-    arguments,
-    options,
-    content,
-    lineno,
-    content_offset,
-    block_text,
-    state,
-    state_machine,
-):
-    if len(missing_imports) == 0:
-        return run(arguments, content, options, state_machine, state, lineno)
-    else:
-        raise ImportError("\n".join(missing_imports))
-
-
-wf_directive.__doc__ = __doc__
-
-
 def _option_boolean(arg):
     if not arg or not arg.strip():
         # no argument given, assume used as a flag
@@ -232,12 +212,12 @@ def mark_wf_labels(app, document):
                     break
 
 
-def setup(app):
-    setup.app = app
-    setup.config = app.config
-    setup.confdir = app.confdir
-
-    options = {
+class WorkflowDirective(Directive):
+    has_content = True
+    required_arguments = 0
+    optional_arguments = 2
+    final_argument_whitespace = False
+    option_spec = {
         "alt": directives.unchanged,
         "height": directives.length_or_unitless,
         "width": directives.length_or_percentage_or_unitless,
@@ -253,7 +233,227 @@ def setup(app):
         "simple_form": _option_boolean,
     }
 
-    app.add_directive("workflow", wf_directive, True, (0, 2, False), **options)
+    def run(self):
+        if missing_imports:
+            raise ImportError("\n".join(missing_imports))
+
+        document = self.state_machine.document
+        config = document.settings.env.config
+        nofigs = "nofigs" in self.options
+
+        formats = get_wf_formats(config)
+        default_fmt = formats[0][0]
+
+        graph2use = self.options.get("graph2use", "hierarchical")
+        simple_form = self.options.get("simple_form", True)
+
+        self.options.setdefault("include-source", config.wf_include_source)
+        keep_context = "context" in self.options
+        context_opt = None if not keep_context else self.options["context"]
+
+        rst_file = document.attributes["source"]
+        rst_dir = os.path.dirname(rst_file)
+
+        if len(self.arguments):
+            if not config.wf_basedir:
+                source_file_name = os.path.join(
+                    setup.app.builder.srcdir, directives.uri(self.arguments[0])
+                )
+            else:
+                source_file_name = os.path.join(
+                    setup.confdir, config.wf_basedir, directives.uri(self.arguments[0])
+                )
+
+            # If there is content, it will be passed as a caption.
+            caption = "\n".join(self.content)
+
+            # If the optional function name is provided, use it
+            if len(self.arguments) == 2:
+                function_name = self.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 self.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 self.options:
+            if self.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 = self.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=self.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 self.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(self.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:
+            self.state_machine.insert_input(total_lines, source=source_file_name)
+
+        # copy image files to builder's output directory, if necessary
+        os.makedirs(dest_dir, exist_ok=True)
+        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
+
+
+def setup(app):
+    setup.app = app
+    setup.config = app.config
+    setup.confdir = app.confdir
+
+    app.add_directive("workflow", WorkflowDirective)
     app.add_config_value("graph2use", "hierarchical", "html")
     app.add_config_value("simple_form", True, "html")
     app.add_config_value("wf_pre_code", None, True)
@@ -548,213 +748,3 @@ def render_figures(
         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
-    os.makedirs(dest_dir, exist_ok=True)
-    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


Test patch: