diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..90fda1aea7 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,12 @@ +on: [status] +jobs: + circleci_artifacts_redirector_job: + runs-on: ubuntu-latest + name: Run CircleCI artifacts redirector + steps: + - name: GitHub Action step + uses: larsoner/circleci-artifacts-redirector-action@master + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + artifact-path: 0/tmp/src/nipype/doc/_build/html/index.html + circleci-jobs: build_docs diff --git a/doc/conf.py b/doc/conf.py index eb34e3f8d8..91fd3086a3 100644 --- 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 # " v 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 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/doc/requirements.txt b/doc/requirements.txt index 057147c5b5..3b0b513ff7 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -5,4 +5,5 @@ nbsphinx sphinx-argparse sphinx>=2.1.2 sphinxcontrib-apidoc -sphinxcontrib-napoleon \ No newline at end of file +sphinxcontrib-napoleon +niflow-nipype1-workflows diff --git a/nipype/sphinxext/plot_workflow.py b/nipype/sphinxext/plot_workflow.py index 78b5f71384..95b87da157 100644 --- 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