From e30cc08c64e081877121f4190e65d896eb7d6fdb Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Tue, 18 Jul 2017 14:08:57 -0400 Subject: [PATCH 1/3] Stop vendoring sphinx-gallery Because sphinx keeps doing incompatible changes which require sphinx_gallery changes to build any docs. --- build_tools/circle/build_doc.sh | 2 +- doc/sphinxext/sphinx_gallery/__init__.py | 12 - .../sphinx_gallery/_static/broken_example.png | Bin 21404 -> 0 bytes .../sphinx_gallery/_static/gallery.css | 192 ------ .../sphinx_gallery/_static/no_image.png | Bin 4315 -> 0 bytes .../sphinx_gallery/backreferences.py | 197 ------ doc/sphinxext/sphinx_gallery/docs_resolv.py | 463 ------------- doc/sphinxext/sphinx_gallery/downloads.py | 120 ---- doc/sphinxext/sphinx_gallery/gen_gallery.py | 304 --------- doc/sphinxext/sphinx_gallery/gen_rst.py | 641 ------------------ doc/sphinxext/sphinx_gallery/notebook.py | 193 ------ .../sphinx_gallery/py_source_parser.py | 99 --- 12 files changed, 1 insertion(+), 2222 deletions(-) delete mode 100644 doc/sphinxext/sphinx_gallery/__init__.py delete mode 100644 doc/sphinxext/sphinx_gallery/_static/broken_example.png delete mode 100644 doc/sphinxext/sphinx_gallery/_static/gallery.css delete mode 100644 doc/sphinxext/sphinx_gallery/_static/no_image.png delete mode 100644 doc/sphinxext/sphinx_gallery/backreferences.py delete mode 100644 doc/sphinxext/sphinx_gallery/docs_resolv.py delete mode 100644 doc/sphinxext/sphinx_gallery/downloads.py delete mode 100644 doc/sphinxext/sphinx_gallery/gen_gallery.py delete mode 100644 doc/sphinxext/sphinx_gallery/gen_rst.py delete mode 100644 doc/sphinxext/sphinx_gallery/notebook.py delete mode 100644 doc/sphinxext/sphinx_gallery/py_source_parser.py diff --git a/build_tools/circle/build_doc.sh b/build_tools/circle/build_doc.sh index 63c8da5aafeac..8788f459769c5 100755 --- a/build_tools/circle/build_doc.sh +++ b/build_tools/circle/build_doc.sh @@ -109,7 +109,7 @@ conda update --yes --quiet conda conda create -n $CONDA_ENV_NAME --yes --quiet python numpy scipy \ cython nose coverage matplotlib sphinx=1.6.2 pillow source activate testenv -pip install numpydoc +pip install numpydoc sphinx-gallery # Build and install scikit-learn in dev mode python setup.py develop diff --git a/doc/sphinxext/sphinx_gallery/__init__.py b/doc/sphinxext/sphinx_gallery/__init__.py deleted file mode 100644 index e113f97d2a2c7..0000000000000 --- a/doc/sphinxext/sphinx_gallery/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Sphinx Gallery -============== - -""" -import os -__version__ = '0.1.11' - - -def glr_path_static(): - """Returns path to packaged static files""" - return os.path.abspath(os.path.join(os.path.dirname(__file__), '_static')) diff --git a/doc/sphinxext/sphinx_gallery/_static/broken_example.png b/doc/sphinxext/sphinx_gallery/_static/broken_example.png deleted file mode 100644 index 4fea24e7df4781c2c32c8d7995511ac89e953145..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21404 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 HW8D7$6^1w` diff --git a/doc/sphinxext/sphinx_gallery/backreferences.py b/doc/sphinxext/sphinx_gallery/backreferences.py deleted file mode 100644 index 32e4dd913f901..0000000000000 --- a/doc/sphinxext/sphinx_gallery/backreferences.py +++ /dev/null @@ -1,197 +0,0 @@ -# -*- coding: utf-8 -*- -# Author: Óscar Nájera -# License: 3-clause BSD -""" -Backreferences Generator -======================== - -Parses example file code in order to keep track of used functions -""" - -from __future__ import print_function -import ast -import os - - -# Try Python 2 first, otherwise load from Python 3 -try: - import cPickle as pickle -except ImportError: - import pickle - - -class NameFinder(ast.NodeVisitor): - """Finds the longest form of variable names and their imports in code - - Only retains names from imported modules. - """ - - def __init__(self): - super(NameFinder, self).__init__() - self.imported_names = {} - self.accessed_names = set() - - def visit_Import(self, node, prefix=''): - for alias in node.names: - local_name = alias.asname or alias.name - self.imported_names[local_name] = prefix + alias.name - - def visit_ImportFrom(self, node): - self.visit_Import(node, node.module + '.') - - def visit_Name(self, node): - self.accessed_names.add(node.id) - - def visit_Attribute(self, node): - attrs = [] - while isinstance(node, ast.Attribute): - attrs.append(node.attr) - node = node.value - - if isinstance(node, ast.Name): - # This is a.b, not e.g. a().b - attrs.append(node.id) - self.accessed_names.add('.'.join(reversed(attrs))) - else: - # need to get a in a().b - self.visit(node) - - def get_mapping(self): - for name in self.accessed_names: - local_name = name.split('.', 1)[0] - remainder = name[len(local_name):] - if local_name in self.imported_names: - # Join import path to relative path - full_name = self.imported_names[local_name] + remainder - yield name, full_name - - -def get_short_module_name(module_name, obj_name): - """ Get the shortest possible module name """ - parts = module_name.split('.') - short_name = module_name - for i in range(len(parts) - 1, 0, -1): - short_name = '.'.join(parts[:i]) - try: - exec('from %s import %s' % (short_name, obj_name)) - except Exception: # libraries can throw all sorts of exceptions... - # get the last working module name - short_name = '.'.join(parts[:(i + 1)]) - break - return short_name - - -def identify_names(code): - """Builds a codeobj summary by identifying and resolving used names - - >>> code = ''' - ... from a.b import c - ... import d as e - ... print(c) - ... e.HelloWorld().f.g - ... ''' - >>> for name, o in sorted(identify_names(code).items()): - ... print(name, o['name'], o['module'], o['module_short']) - c c a.b a.b - e.HelloWorld HelloWorld d d - """ - finder = NameFinder() - try: - finder.visit(ast.parse(code)) - except SyntaxError: - return {} - - example_code_obj = {} - for name, full_name in finder.get_mapping(): - # name is as written in file (e.g. np.asarray) - # full_name includes resolved import path (e.g. numpy.asarray) - splitted = full_name.rsplit('.', 1) - if len(splitted) == 1: - # module without attribute. This is not useful for - # backreferences - continue - - module, attribute = splitted - # get shortened module name - module_short = get_short_module_name(module, attribute) - cobj = {'name': attribute, 'module': module, - 'module_short': module_short} - example_code_obj[name] = cobj - return example_code_obj - - -def scan_used_functions(example_file, gallery_conf): - """save variables so we can later add links to the documentation""" - example_code_obj = identify_names(open(example_file).read()) - if example_code_obj: - codeobj_fname = example_file[:-3] + '_codeobj.pickle' - with open(codeobj_fname, 'wb') as fid: - pickle.dump(example_code_obj, fid, pickle.HIGHEST_PROTOCOL) - - backrefs = set('{module_short}.{name}'.format(**entry) - for entry in example_code_obj.values() - if entry['module'].startswith(gallery_conf['doc_module'])) - - return backrefs - - -THUMBNAIL_TEMPLATE = """ -.. raw:: html - -
- -.. only:: html - - .. figure:: /{thumbnail} - - :ref:`sphx_glr_{ref_name}` - -.. raw:: html - -
-""" - -BACKREF_THUMBNAIL_TEMPLATE = THUMBNAIL_TEMPLATE + """ -.. only:: not html - - * :ref:`sphx_glr_{ref_name}` -""" - - -def _thumbnail_div(full_dir, fname, snippet, is_backref=False): - """Generates RST to place a thumbnail in a gallery""" - thumb = os.path.join(full_dir, 'images', 'thumb', - 'sphx_glr_%s_thumb.png' % fname[:-3]) - - # Inside rst files forward slash defines paths - thumb = thumb.replace(os.sep, "/") - - ref_name = os.path.join(full_dir, fname).replace(os.path.sep, '_') - - template = BACKREF_THUMBNAIL_TEMPLATE if is_backref else THUMBNAIL_TEMPLATE - return template.format(snippet=snippet, thumbnail=thumb, ref_name=ref_name) - - -def write_backreferences(seen_backrefs, gallery_conf, - target_dir, fname, snippet): - """Writes down back reference files, which include a thumbnail list - of examples using a certain module""" - if gallery_conf['backreferences_dir'] is None: - return - - example_file = os.path.join(target_dir, fname) - build_target_dir = os.path.relpath(target_dir, gallery_conf['src_dir']) - backrefs = scan_used_functions(example_file, gallery_conf) - for backref in backrefs: - include_path = os.path.join(gallery_conf['src_dir'], - gallery_conf['backreferences_dir'], - '%s.examples' % backref) - seen = backref in seen_backrefs - with open(include_path, 'a' if seen else 'w') as ex_file: - if not seen: - heading = '\n\nExamples using ``%s``' % backref - ex_file.write(heading + '\n') - ex_file.write('^' * len(heading) + '\n') - ex_file.write(_thumbnail_div(build_target_dir, fname, snippet, - is_backref=True)) - seen_backrefs.add(backref) diff --git a/doc/sphinxext/sphinx_gallery/docs_resolv.py b/doc/sphinxext/sphinx_gallery/docs_resolv.py deleted file mode 100644 index 0f9943b683d1c..0000000000000 --- a/doc/sphinxext/sphinx_gallery/docs_resolv.py +++ /dev/null @@ -1,463 +0,0 @@ -# -*- coding: utf-8 -*- -# Author: Óscar Nájera -# License: 3-clause BSD -""" -Link resolver objects -===================== -""" -from __future__ import print_function -import gzip -import os -import posixpath -import re -import shelve -import sys - -from sphinx.util.console import fuchsia - -# Try Python 2 first, otherwise load from Python 3 -try: - import cPickle as pickle - import urllib2 as urllib - from urllib2 import HTTPError, URLError -except ImportError: - import pickle - import urllib.request - import urllib.error - import urllib.parse - from urllib.error import HTTPError, URLError - -from io import StringIO - - -def _get_data(url): - """Helper function to get data over http or from a local file""" - if url.startswith('http://'): - # Try Python 2, use Python 3 on exception - try: - resp = urllib.urlopen(url) - encoding = resp.headers.dict.get('content-encoding', 'plain') - except AttributeError: - resp = urllib.request.urlopen(url) - encoding = resp.headers.get('content-encoding', 'plain') - data = resp.read() - if encoding == 'plain': - pass - elif encoding == 'gzip': - data = StringIO(data) - data = gzip.GzipFile(fileobj=data).read() - else: - raise RuntimeError('unknown encoding') - else: - with open(url, 'r') as fid: - data = fid.read() - - return data - - -def get_data(url, gallery_dir): - """Persistent dictionary usage to retrieve the search indexes""" - - # shelve keys need to be str in python 2 - if sys.version_info[0] == 2 and isinstance(url, unicode): - url = url.encode('utf-8') - - cached_file = os.path.join(gallery_dir, 'searchindex') - search_index = shelve.open(cached_file) - if url in search_index: - data = search_index[url] - else: - data = _get_data(url) - search_index[url] = data - search_index.close() - - return data - - -def _select_block(str_in, start_tag, end_tag): - """Select first block delimited by start_tag and end_tag""" - start_pos = str_in.find(start_tag) - if start_pos < 0: - raise ValueError('start_tag not found') - depth = 0 - for pos in range(start_pos, len(str_in)): - if str_in[pos] == start_tag: - depth += 1 - elif str_in[pos] == end_tag: - depth -= 1 - - if depth == 0: - break - sel = str_in[start_pos + 1:pos] - return sel - - -def _parse_dict_recursive(dict_str): - """Parse a dictionary from the search index""" - dict_out = dict() - pos_last = 0 - pos = dict_str.find(':') - while pos >= 0: - key = dict_str[pos_last:pos] - if dict_str[pos + 1] == '[': - # value is a list - pos_tmp = dict_str.find(']', pos + 1) - if pos_tmp < 0: - raise RuntimeError('error when parsing dict') - value = dict_str[pos + 2: pos_tmp].split(',') - # try to convert elements to int - for i in range(len(value)): - try: - value[i] = int(value[i]) - except ValueError: - pass - elif dict_str[pos + 1] == '{': - # value is another dictionary - subdict_str = _select_block(dict_str[pos:], '{', '}') - value = _parse_dict_recursive(subdict_str) - pos_tmp = pos + len(subdict_str) - else: - raise ValueError('error when parsing dict: unknown elem') - - key = key.strip('"') - if len(key) > 0: - dict_out[key] = value - - pos_last = dict_str.find(',', pos_tmp) - if pos_last < 0: - break - pos_last += 1 - pos = dict_str.find(':', pos_last) - - return dict_out - - -def parse_sphinx_searchindex(searchindex): - """Parse a Sphinx search index - - Parameters - ---------- - searchindex : str - The Sphinx search index (contents of searchindex.js) - - Returns - ------- - filenames : list of str - The file names parsed from the search index. - objects : dict - The objects parsed from the search index. - """ - # Make sure searchindex uses UTF-8 encoding - if hasattr(searchindex, 'decode'): - searchindex = searchindex.decode('UTF-8') - - # parse objects - query = 'objects:' - pos = searchindex.find(query) - if pos < 0: - raise ValueError('"objects:" not found in search index') - - sel = _select_block(searchindex[pos:], '{', '}') - objects = _parse_dict_recursive(sel) - - # parse filenames - query = 'filenames:' - pos = searchindex.find(query) - if pos < 0: - raise ValueError('"filenames:" not found in search index') - filenames = searchindex[pos + len(query) + 1:] - filenames = filenames[:filenames.find(']')] - filenames = [f.strip('"') for f in filenames.split(',')] - - return filenames, objects - - -class SphinxDocLinkResolver(object): - """ Resolve documentation links using searchindex.js generated by Sphinx - - Parameters - ---------- - doc_url : str - The base URL of the project website. - searchindex : str - Filename of searchindex, relative to doc_url. - extra_modules_test : list of str - List of extra module names to test. - relative : bool - Return relative links (only useful for links to documentation of this - package). - """ - - def __init__(self, doc_url, gallery_dir, searchindex='searchindex.js', - extra_modules_test=None, relative=False): - self.doc_url = doc_url - self.gallery_dir = gallery_dir - self.relative = relative - self._link_cache = {} - - self.extra_modules_test = extra_modules_test - self._page_cache = {} - if doc_url.startswith('http://'): - if relative: - raise ValueError('Relative links are only supported for local ' - 'URLs (doc_url cannot start with "http://)"') - searchindex_url = doc_url + '/' + searchindex - else: - searchindex_url = os.path.join(doc_url, searchindex) - - # detect if we are using relative links on a Windows system - if os.name.lower() == 'nt' and not doc_url.startswith('http://'): - if not relative: - raise ValueError('You have to use relative=True for the local' - ' package on a Windows system.') - self._is_windows = True - else: - self._is_windows = False - - # download and initialize the search index - sindex = get_data(searchindex_url, gallery_dir) - filenames, objects = parse_sphinx_searchindex(sindex) - - self._searchindex = dict(filenames=filenames, objects=objects) - - def _get_link(self, cobj): - """Get a valid link, False if not found""" - - fname_idx = None - full_name = cobj['module_short'] + '.' + cobj['name'] - if full_name in self._searchindex['objects']: - value = self._searchindex['objects'][full_name] - if isinstance(value, dict): - value = value[next(iter(value.keys()))] - fname_idx = value[0] - elif cobj['module_short'] in self._searchindex['objects']: - value = self._searchindex['objects'][cobj['module_short']] - if cobj['name'] in value.keys(): - fname_idx = value[cobj['name']][0] - - if fname_idx is not None: - fname = self._searchindex['filenames'][fname_idx] - # In 1.5+ Sphinx seems to have changed from .rst.html to only - # .html extension in converted files. But URLs could be - # built with < 1.5 or >= 1.5 regardless of what we're currently - # building with, so let's just check both :( - fnames = [fname + '.html', os.path.splitext(fname)[0] + '.html'] - for fname in fnames: - try: - if self._is_windows: - fname = fname.replace('/', '\\') - link = os.path.join(self.doc_url, fname) - else: - link = posixpath.join(self.doc_url, fname) - - if hasattr(link, 'decode'): - link = link.decode('utf-8', 'replace') - - if link in self._page_cache: - html = self._page_cache[link] - else: - html = get_data(link, self.gallery_dir) - self._page_cache[link] = html - except (HTTPError, URLError, IOError): - pass - else: - break - else: - raise - - # test if cobj appears in page - comb_names = [cobj['module_short'] + '.' + cobj['name']] - if self.extra_modules_test is not None: - for mod in self.extra_modules_test: - comb_names.append(mod + '.' + cobj['name']) - url = False - if hasattr(html, 'decode'): - # Decode bytes under Python 3 - html = html.decode('utf-8', 'replace') - - for comb_name in comb_names: - if hasattr(comb_name, 'decode'): - # Decode bytes under Python 3 - comb_name = comb_name.decode('utf-8', 'replace') - if comb_name in html: - url = link + u'#' + comb_name - link = url - else: - link = False - - return link - - def resolve(self, cobj, this_url): - """Resolve the link to the documentation, returns None if not found - - Parameters - ---------- - cobj : dict - Dict with information about the "code object" for which we are - resolving a link. - cobj['name'] : function or class name (str) - cobj['module_short'] : shortened module name (str) - cobj['module'] : module name (str) - this_url: str - URL of the current page. Needed to construct relative URLs - (only used if relative=True in constructor). - - Returns - ------- - link : str | None - The link (URL) to the documentation. - """ - full_name = cobj['module_short'] + '.' + cobj['name'] - link = self._link_cache.get(full_name, None) - if link is None: - # we don't have it cached - link = self._get_link(cobj) - # cache it for the future - self._link_cache[full_name] = link - - if link is False or link is None: - # failed to resolve - return None - - if self.relative: - link = os.path.relpath(link, start=this_url) - if self._is_windows: - # replace '\' with '/' so it on the web - link = link.replace('\\', '/') - - # for some reason, the relative link goes one directory too high up - link = link[3:] - - return link - - -def _embed_code_links(app, gallery_conf, gallery_dir): - # Add resolvers for the packages for which we want to show links - doc_resolvers = {} - - src_gallery_dir = os.path.join(app.builder.srcdir, gallery_dir) - for this_module, url in gallery_conf['reference_url'].items(): - try: - if url is None: - doc_resolvers[this_module] = SphinxDocLinkResolver( - app.builder.outdir, - src_gallery_dir, - relative=True) - else: - doc_resolvers[this_module] = SphinxDocLinkResolver(url, - src_gallery_dir) - - except HTTPError as e: - print("The following HTTP Error has occurred:\n") - print(e.code) - except URLError as e: - print("\n...\n" - "Warning: Embedding the documentation hyperlinks requires " - "Internet access.\nPlease check your network connection.\n" - "Unable to continue embedding `{0}` links due to a URL " - "Error:\n".format(this_module)) - print(e.args) - - html_gallery_dir = os.path.abspath(os.path.join(app.builder.outdir, - gallery_dir)) - - # patterns for replacement - link_pattern = ('%s') - orig_pattern = '%s' - period = '.' - - # This could be turned into a generator if necessary, but should be okay - flat = [[dirpath, filename] - for dirpath, _, filenames in os.walk(html_gallery_dir) - for filename in filenames] - iterator = app.status_iterator( - flat, os.path.basename(html_gallery_dir), colorfunc=fuchsia, - length=len(flat), stringify_func=lambda x: os.path.basename(x[1])) - for dirpath, fname in iterator: - full_fname = os.path.join(html_gallery_dir, dirpath, fname) - subpath = dirpath[len(html_gallery_dir) + 1:] - pickle_fname = os.path.join(src_gallery_dir, subpath, - fname[:-5] + '_codeobj.pickle') - - if os.path.exists(pickle_fname): - # we have a pickle file with the objects to embed links for - with open(pickle_fname, 'rb') as fid: - example_code_obj = pickle.load(fid) - fid.close() - str_repl = {} - # generate replacement strings with the links - for name, cobj in example_code_obj.items(): - this_module = cobj['module'].split('.')[0] - - if this_module not in doc_resolvers: - continue - - try: - link = doc_resolvers[this_module].resolve(cobj, - full_fname) - except (HTTPError, URLError) as e: - if isinstance(e, HTTPError): - extra = e.code - else: - extra = e.reason - print("\n\t\tError resolving %s.%s: %r (%s)" - % (cobj['module'], cobj['name'], e, extra)) - continue - - if link is not None: - parts = name.split('.') - name_html = period.join(orig_pattern % part - for part in parts) - full_function_name = '%s.%s' % ( - cobj['module'], cobj['name']) - str_repl[name_html] = link_pattern % ( - link, full_function_name, name_html) - # do the replacement in the html file - - # ensure greediness - names = sorted(str_repl, key=len, reverse=True) - regex_str = '|'.join(re.escape(name) for name in names) - regex = re.compile(regex_str) - - def substitute_link(match): - return str_repl[match.group()] - - if len(str_repl) > 0: - with open(full_fname, 'rb') as fid: - lines_in = fid.readlines() - with open(full_fname, 'wb') as fid: - for line in lines_in: - line = line.decode('utf-8') - line = regex.sub(substitute_link, line) - fid.write(line.encode('utf-8')) - - -def embed_code_links(app, exception): - """Embed hyperlinks to documentation into example code""" - if exception is not None: - return - - # No need to waste time embedding hyperlinks when not running the examples - # XXX: also at the time of writing this fixes make html-noplot - # for some reason I don't fully understand - if not app.builder.config.plot_gallery: - return - - # XXX: Whitelist of builders for which it makes sense to embed - # hyperlinks inside the example html. Note that the link embedding - # require searchindex.js to exist for the links to the local doc - # and there does not seem to be a good way of knowing which - # builders creates a searchindex.js. - if app.builder.name not in ['html', 'readthedocs']: - return - - print('Embedding documentation hyperlinks in examples..') - - gallery_conf = app.config.sphinx_gallery_conf - - gallery_dirs = gallery_conf['gallery_dirs'] - if not isinstance(gallery_dirs, list): - gallery_dirs = [gallery_dirs] - - for gallery_dir in gallery_dirs: - _embed_code_links(app, gallery_conf, gallery_dir) diff --git a/doc/sphinxext/sphinx_gallery/downloads.py b/doc/sphinxext/sphinx_gallery/downloads.py deleted file mode 100644 index 6b5b3df17fc87..0000000000000 --- a/doc/sphinxext/sphinx_gallery/downloads.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- coding: utf-8 -*- -r""" -Utilities for downloadable items -================================ - -""" -# Author: Óscar Nájera -# License: 3-clause BSD - -from __future__ import absolute_import, division, print_function - -import os -import zipfile - -CODE_DOWNLOAD = """ -\n.. container:: sphx-glr-footer - -\n .. container:: sphx-glr-download - - :download:`Download Python source code: {0} <{0}>`\n - -\n .. container:: sphx-glr-download - - :download:`Download Jupyter notebook: {1} <{1}>`\n""" - -CODE_ZIP_DOWNLOAD = """ -\n.. container:: sphx-glr-footer - -\n .. container:: sphx-glr-download - - :download:`Download all examples in Python source code: {0} `\n - -\n .. container:: sphx-glr-download - - :download:`Download all examples in Jupyter notebooks: {2} `\n""" - - -def python_zip(file_list, gallery_path, extension='.py'): - """Stores all files in file_list into an zip file - - Parameters - ---------- - file_list : list of strings - Holds all the file names to be included in zip file - gallery_path : string - path to where the zipfile is stored - extension : str - '.py' or '.ipynb' In order to deal with downloads of python - sources and jupyter notebooks the file extension from files in - file_list will be removed and replace with the value of this - variable while generating the zip file - Returns - ------- - zipname : string - zip file name, written as `target_dir_{python,jupyter}.zip` - depending on the extension - """ - zipname = os.path.basename(gallery_path) - zipname += '_python' if extension == '.py' else '_jupyter' - zipname = os.path.join(gallery_path, zipname + '.zip') - - zipf = zipfile.ZipFile(zipname, mode='w') - for fname in file_list: - file_src = os.path.splitext(fname)[0] + extension - zipf.write(file_src, os.path.relpath(file_src, gallery_path)) - zipf.close() - - return zipname - - -def list_downloadable_sources(target_dir): - """Returns a list of python source files is target_dir - - Parameters - ---------- - target_dir : string - path to the directory where python source file are - Returns - ------- - list - list of paths to all Python source files in `target_dir` - """ - return [os.path.join(target_dir, fname) - for fname in os.listdir(target_dir) - if fname.endswith('.py')] - - -def generate_zipfiles(gallery_dir): - """ - Collects all Python source files and Jupyter notebooks in - gallery_dir and makes zipfiles of them - - Parameters - ---------- - gallery_dir : string - path of the gallery to collect downloadable sources - - Return - ------ - download_rst: string - RestructuredText to include download buttons to the generated files - """ - - listdir = list_downloadable_sources(gallery_dir) - for directory in sorted(os.listdir(gallery_dir)): - if os.path.isdir(os.path.join(gallery_dir, directory)): - target_dir = os.path.join(gallery_dir, directory) - listdir.extend(list_downloadable_sources(target_dir)) - - py_zipfile = python_zip(listdir, gallery_dir) - jy_zipfile = python_zip(listdir, gallery_dir, ".ipynb") - - def rst_path(filepath): - return filepath.replace(os.sep, '/') - - dw_rst = CODE_ZIP_DOWNLOAD.format(os.path.basename(py_zipfile), - rst_path(py_zipfile), - os.path.basename(jy_zipfile), - rst_path(jy_zipfile)) - return dw_rst diff --git a/doc/sphinxext/sphinx_gallery/gen_gallery.py b/doc/sphinxext/sphinx_gallery/gen_gallery.py deleted file mode 100644 index 1a1ce299fab1c..0000000000000 --- a/doc/sphinxext/sphinx_gallery/gen_gallery.py +++ /dev/null @@ -1,304 +0,0 @@ -# -*- coding: utf-8 -*- -# Author: Óscar Nájera -# License: 3-clause BSD -""" -Sphinx-Gallery Generator -======================== - -Attaches Sphinx-Gallery to Sphinx in order to generate the galleries -when building the documentation. -""" - - -from __future__ import division, print_function, absolute_import -import copy -import re -import os - -from . import glr_path_static -from .gen_rst import generate_dir_rst, SPHX_GLR_SIG -from .docs_resolv import embed_code_links -from .downloads import generate_zipfiles - -try: - FileNotFoundError -except NameError: - # Python2 - FileNotFoundError = IOError - -DEFAULT_GALLERY_CONF = { - 'filename_pattern': re.escape(os.sep) + 'plot', - 'examples_dirs': os.path.join('..', 'examples'), - 'gallery_dirs': 'auto_examples', - 'backreferences_dir': None, - 'doc_module': (), - 'reference_url': {}, - # build options - 'plot_gallery': True, - 'download_all_examples': True, - 'abort_on_example_error': False, - 'failing_examples': {}, - 'expected_failing_examples': set(), -} - - -def clean_gallery_out(build_dir): - """Deletes images under the sphx_glr namespace in the build directory""" - # Sphinx hack: sphinx copies generated images to the build directory - # each time the docs are made. If the desired image name already - # exists, it appends a digit to prevent overwrites. The problem is, - # the directory is never cleared. This means that each time you build - # the docs, the number of images in the directory grows. - # - # This question has been asked on the sphinx development list, but there - # was no response: http://osdir.com/ml/sphinx-dev/2011-02/msg00123.html - # - # The following is a hack that prevents this behavior by clearing the - # image build directory from gallery images each time the docs are built. - # If sphinx changes their layout between versions, this will not - # work (though it should probably not cause a crash). - # Tested successfully on Sphinx 1.0.7 - - build_image_dir = os.path.join(build_dir, '_images') - if os.path.exists(build_image_dir): - filelist = os.listdir(build_image_dir) - for filename in filelist: - if filename.startswith('sphx_glr') and filename.endswith('png'): - os.remove(os.path.join(build_image_dir, filename)) - - -def parse_config(app): - """Process the Sphinx Gallery configuration""" - # TODO: Test this behavior. - try: - plot_gallery = eval(app.builder.config.plot_gallery) - except TypeError: - plot_gallery = bool(app.builder.config.plot_gallery) - - gallery_conf = copy.deepcopy(DEFAULT_GALLERY_CONF) - gallery_conf.update(app.config.sphinx_gallery_conf) - gallery_conf.update(plot_gallery=plot_gallery) - gallery_conf.update( - abort_on_example_error=app.builder.config.abort_on_example_error) - gallery_conf['src_dir'] = app.builder.srcdir - - backreferences_warning = """\n======== -Sphinx-Gallery now requires you to set the configuration variable -'backreferences_dir' in your config to activate the -backreferences. That is mini galleries clustered by the functions used -in the example scripts. Have a look at it in sphinx-gallery - -https://sphinx-gallery.readthedocs.io/en/stable/index.html#examples-using-numpy-linspace -""" - - if gallery_conf.get("mod_example_dir", False): - update_msg = """\nFor a quick fix try replacing 'mod_example_dir' -by 'backreferences_dir' in your conf.py file. If that does not solve the -present issue read carefully how to update in the online documentation - -https://sphinx-gallery.readthedocs.io/en/latest/advanced_configuration.html#references-to-examples""" - - gallery_conf['backreferences_dir'] = gallery_conf['mod_example_dir'] - app.warn("Old configuration for backreferences detected \n" - "using the configuration variable `mod_example_dir`\n" - + backreferences_warning - + update_msg, prefix="DeprecationWarning: ") - - elif gallery_conf['backreferences_dir'] is None: - no_care_msg = """ -If you don't care about this features set in your conf.py -'backreferences_dir': False\n""" - - app.warn(backreferences_warning + no_care_msg) - - gallery_conf['backreferences_dir'] = os.path.join( - 'modules', 'generated') - app.warn("using old default 'backreferences_dir':'{}'.\n" - " This will be disabled in future releases\n".format( - gallery_conf['backreferences_dir']), - prefix="DeprecationWarning: ") - - # this assures I can call the config in other places - app.config.sphinx_gallery_conf = gallery_conf - app.config.html_static_path.append(glr_path_static()) - - return gallery_conf - - -def _prepare_sphx_glr_dirs(gallery_conf, srcdir): - """Creates necessary folders for sphinx_gallery files """ - examples_dirs = gallery_conf['examples_dirs'] - gallery_dirs = gallery_conf['gallery_dirs'] - - if not isinstance(examples_dirs, list): - examples_dirs = [examples_dirs] - if not isinstance(gallery_dirs, list): - gallery_dirs = [gallery_dirs] - - if bool(gallery_conf['backreferences_dir']): - backreferences_dir = os.path.join( - srcdir, gallery_conf['backreferences_dir']) - if not os.path.exists(backreferences_dir): - os.makedirs(backreferences_dir) - - return examples_dirs, gallery_dirs - - -def generate_gallery_rst(app): - """Generate the Main examples gallery reStructuredText - - Start the sphinx-gallery configuration and recursively scan the examples - directories in order to populate the examples gallery - """ - print('Generating gallery') - gallery_conf = parse_config(app) - - clean_gallery_out(app.builder.outdir) - - seen_backrefs = set() - - computation_times = [] - examples_dirs, gallery_dirs = _prepare_sphx_glr_dirs(gallery_conf, - app.builder.srcdir) - - for examples_dir, gallery_dir in zip(examples_dirs, gallery_dirs): - examples_dir = os.path.join(app.builder.srcdir, examples_dir) - gallery_dir = os.path.join(app.builder.srcdir, gallery_dir) - - for workdir in [examples_dir, gallery_dir]: - if not os.path.exists(workdir): - os.makedirs(workdir) - # Here we don't use an os.walk, but we recurse only twice: flat is - # better than nested. - this_fhindex, this_computation_times = generate_dir_rst( - examples_dir, gallery_dir, gallery_conf, seen_backrefs) - if this_fhindex == "": - raise FileNotFoundError("Main example directory {0} does not " - "have a README.txt file. Please write " - "one to introduce your gallery." - .format(examples_dir)) - - computation_times += this_computation_times - - # we create an index.rst with all examples - fhindex = open(os.path.join(gallery_dir, 'index.rst'), 'w') - # :orphan: to suppress "not included in TOCTREE" sphinx warnings - fhindex.write(":orphan:\n\n" + this_fhindex) - for directory in sorted(os.listdir(examples_dir)): - if os.path.isdir(os.path.join(examples_dir, directory)): - src_dir = os.path.join(examples_dir, directory) - target_dir = os.path.join(gallery_dir, directory) - this_fhindex, this_computation_times = generate_dir_rst(src_dir, target_dir, gallery_conf, - seen_backrefs) - fhindex.write(this_fhindex) - computation_times += this_computation_times - - if gallery_conf['download_all_examples']: - download_fhindex = generate_zipfiles(gallery_dir) - fhindex.write(download_fhindex) - - fhindex.write(SPHX_GLR_SIG) - fhindex.flush() - - if gallery_conf['plot_gallery']: - print("Computation time summary:") - for time_elapsed, fname in sorted(computation_times)[::-1]: - if time_elapsed is not None: - print("\t- %s : %.2g sec" % (fname, time_elapsed)) - else: - print("\t- %s : not run" % fname) - - -def touch_empty_backreferences(app, what, name, obj, options, lines): - """Generate empty back-reference example files - - This avoids inclusion errors/warnings if there are no gallery - examples for a class / module that is being parsed by autodoc""" - - if not bool(app.config.sphinx_gallery_conf['backreferences_dir']): - return - - examples_path = os.path.join(app.srcdir, - app.config.sphinx_gallery_conf[ - "backreferences_dir"], - "%s.examples" % name) - - if not os.path.exists(examples_path): - # touch file - open(examples_path, 'w').close() - - -def sumarize_failing_examples(app, exception): - """Collects the list of falling examples during build and prints them with the traceback - - Raises ValueError if there where failing examples - """ - if exception is not None: - return - - # Under no-plot Examples are not run so nothing to summarize - if not app.config.sphinx_gallery_conf['plot_gallery']: - return - - gallery_conf = app.config.sphinx_gallery_conf - failing_examples = set(gallery_conf['failing_examples'].keys()) - expected_failing_examples = set([os.path.normpath(os.path.join(app.srcdir, path)) - for path in - gallery_conf['expected_failing_examples']]) - - examples_expected_to_fail = failing_examples.intersection( - expected_failing_examples) - expected_fail_msg = [] - if examples_expected_to_fail: - expected_fail_msg.append("\n\nExamples failing as expected:") - for fail_example in examples_expected_to_fail: - expected_fail_msg.append(fail_example + ' failed leaving traceback:\n' + - gallery_conf['failing_examples'][fail_example] + '\n') - print("\n".join(expected_fail_msg)) - - examples_not_expected_to_fail = failing_examples.difference( - expected_failing_examples) - fail_msgs = [] - if examples_not_expected_to_fail: - fail_msgs.append("Unexpected failing examples:") - for fail_example in examples_not_expected_to_fail: - fail_msgs.append(fail_example + ' failed leaving traceback:\n' + - gallery_conf['failing_examples'][fail_example] + '\n') - - examples_not_expected_to_pass = expected_failing_examples.difference( - failing_examples) - if examples_not_expected_to_pass: - fail_msgs.append("Examples expected to fail, but not failling:\n" + - "Please remove these examples from\n" + - "sphinx_gallery_conf['expected_failing_examples']\n" + - "in your conf.py file" - "\n".join(examples_not_expected_to_pass)) - - if fail_msgs: - raise ValueError("Here is a summary of the problems encountered when " - "running the examples\n\n" + "\n".join(fail_msgs) + - "\n" + "-" * 79) - - -def get_default_config_value(key): - def default_getter(conf): - return conf['sphinx_gallery_conf'].get(key, DEFAULT_GALLERY_CONF[key]) - return default_getter - - -def setup(app): - """Setup sphinx-gallery sphinx extension""" - app.add_config_value('sphinx_gallery_conf', DEFAULT_GALLERY_CONF, 'html') - for key in ['plot_gallery', 'abort_on_example_error']: - app.add_config_value(key, get_default_config_value(key), 'html') - - app.add_stylesheet('gallery.css') - # Sphinx < 1.6 calls it `_extensions`, >= 1.6 is `extensions`. - extensions_attr = '_extensions' if hasattr(app, '_extensions') else 'extensions' - if 'sphinx.ext.autodoc' in getattr(app, extensions_attr): - app.connect('autodoc-process-docstring', touch_empty_backreferences) - - app.connect('builder-inited', generate_gallery_rst) - - app.connect('build-finished', sumarize_failing_examples) - app.connect('build-finished', embed_code_links) diff --git a/doc/sphinxext/sphinx_gallery/gen_rst.py b/doc/sphinxext/sphinx_gallery/gen_rst.py deleted file mode 100644 index c2a0b95545499..0000000000000 --- a/doc/sphinxext/sphinx_gallery/gen_rst.py +++ /dev/null @@ -1,641 +0,0 @@ -# -*- coding: utf-8 -*- -# Author: Óscar Nájera -# License: 3-clause BSD -""" -RST file generator -================== - -Generate the rst files for the examples by iterating over the python -example files. - -Files that generate images should start with 'plot' - -""" -# Don't use unicode_literals here (be explicit with u"..." instead) otherwise -# tricky errors come up with exec(code_blocks, ...) calls -from __future__ import division, print_function, absolute_import -from time import time -import codecs -import hashlib -import os -import re -import shutil -import subprocess -import sys -import traceback -import warnings - - -# Try Python 2 first, otherwise load from Python 3 -try: - # textwrap indent only exists in python 3 - from textwrap import indent -except ImportError: - def indent(text, prefix, predicate=None): - """Adds 'prefix' to the beginning of selected lines in 'text'. - - If 'predicate' is provided, 'prefix' will only be added to the lines - where 'predicate(line)' is True. If 'predicate' is not provided, - it will default to adding 'prefix' to all non-empty lines that do not - consist solely of whitespace characters. - """ - if predicate is None: - def predicate(line): - return line.strip() - - def prefixed_lines(): - for line in text.splitlines(True): - yield (prefix + line if predicate(line) else line) - return ''.join(prefixed_lines()) - -from io import StringIO - -# make sure that the Agg backend is set before importing any -# matplotlib -import matplotlib -matplotlib.use('agg') -matplotlib_backend = matplotlib.get_backend() - -if matplotlib_backend != 'agg': - mpl_backend_msg = ( - "Sphinx-Gallery relies on the matplotlib 'agg' backend to " - "render figures and write them to files. You are " - "currently using the {} backend. Sphinx-Gallery will " - "terminate the build now, because changing backends is " - "not well supported by matplotlib. We advise you to move " - "sphinx_gallery imports before any matplotlib-dependent " - "import. Moving sphinx_gallery imports at the top of " - "your conf.py file should fix this issue") - - raise ValueError(mpl_backend_msg.format(matplotlib_backend)) - -import matplotlib.pyplot as plt - -from . import glr_path_static -from .backreferences import write_backreferences, _thumbnail_div -from .downloads import CODE_DOWNLOAD -from .py_source_parser import (get_docstring_and_rest, - split_code_and_text_blocks) - -from .notebook import jupyter_notebook, save_notebook - -try: - basestring -except NameError: - basestring = str - unicode = str - - -############################################################################### - - -class Tee(object): - """A tee object to redirect streams to multiple outputs""" - - def __init__(self, file1, file2): - self.file1 = file1 - self.file2 = file2 - - def write(self, data): - self.file1.write(data) - self.file2.write(data) - - def flush(self): - self.file1.flush() - self.file2.flush() - - # When called from a local terminal seaborn needs it in Python3 - def isatty(self): - self.file1.isatty() - - -class MixedEncodingStringIO(StringIO): - """Helper when both ASCII and unicode strings will be written""" - - def write(self, data): - if not isinstance(data, unicode): - data = data.decode('utf-8') - StringIO.write(self, data) - - -############################################################################### -# The following strings are used when we have several pictures: we use -# an html div tag that our CSS uses to turn the lists into horizontal -# lists. -HLIST_HEADER = """ -.. rst-class:: sphx-glr-horizontal - -""" - -HLIST_IMAGE_TEMPLATE = """ - * - - .. image:: /%s - :scale: 47 -""" - -SINGLE_IMAGE = """ -.. image:: /%s - :align: center -""" - - -# This one could contain unicode -CODE_OUTPUT = u""".. rst-class:: sphx-glr-script-out - - Out:: - -{0}\n""" - - -SPHX_GLR_SIG = """\n.. rst-class:: sphx-glr-signature - - `Generated by Sphinx-Gallery `_\n""" - - -def codestr2rst(codestr, lang='python'): - """Return reStructuredText code block from code string""" - code_directive = "\n.. code-block:: {0}\n\n".format(lang) - indented_block = indent(codestr, ' ' * 4) - return code_directive + indented_block - - -def extract_thumbnail_number(text): - """ Pull out the thumbnail image number specified in the docstring. """ - - # check whether the user has specified a specific thumbnail image - pattr = re.compile( - r"^\s*#\s*sphinx_gallery_thumbnail_number\s*=\s*([0-9]+)\s*$", - flags=re.MULTILINE) - match = pattr.search(text) - - if match is None: - # by default, use the first figure created - thumbnail_number = 1 - else: - thumbnail_number = int(match.groups()[0]) - - return thumbnail_number - - -def extract_intro(filename): - """ Extract the first paragraph of module-level docstring. max:95 char""" - - docstring, _ = get_docstring_and_rest(filename) - - # lstrip is just in case docstring has a '\n\n' at the beginning - paragraphs = docstring.lstrip().split('\n\n') - if len(paragraphs) > 1: - first_paragraph = re.sub('\n', ' ', paragraphs[1]) - first_paragraph = (first_paragraph[:95] + '...' - if len(first_paragraph) > 95 else first_paragraph) - else: - raise ValueError( - "Example docstring should have a header for the example title " - "and at least a paragraph explaining what the example is about. " - "Please check the example file:\n {}\n".format(filename)) - - return first_paragraph - - -def get_md5sum(src_file): - """Returns md5sum of file""" - - with open(src_file, 'rb') as src_data: - src_content = src_data.read() - - src_md5 = hashlib.md5(src_content).hexdigest() - return src_md5 - - -def md5sum_is_current(src_file): - """Checks whether src_file has the same md5 hash as the one on disk""" - - src_md5 = get_md5sum(src_file) - - src_md5_file = src_file + '.md5' - if os.path.exists(src_md5_file): - with open(src_md5_file, 'r') as file_checksum: - ref_md5 = file_checksum.read() - - return src_md5 == ref_md5 - - return False - - -def save_figures(image_path, fig_count, gallery_conf): - """Save all open matplotlib figures of the example code-block - - Parameters - ---------- - image_path : str - Path where plots are saved (format string which accepts figure number) - fig_count : int - Previous figure number count. Figure number add from this number - gallery_conf : dict - Contains the configuration of Sphinx-Gallery - - Returns - ------- - images_rst : str - rst code to embed the images in the document - fig_num : int - number of figures saved - """ - figure_list = [] - - for fig_num in plt.get_fignums(): - # Set the fig_num figure as the current figure as we can't - # save a figure that's not the current figure. - fig = plt.figure(fig_num) - kwargs = {} - to_rgba = matplotlib.colors.colorConverter.to_rgba - for attr in ['facecolor', 'edgecolor']: - fig_attr = getattr(fig, 'get_' + attr)() - default_attr = matplotlib.rcParams['figure.' + attr] - if to_rgba(fig_attr) != to_rgba(default_attr): - kwargs[attr] = fig_attr - - current_fig = image_path.format(fig_count + fig_num) - fig.savefig(current_fig, **kwargs) - figure_list.append(current_fig) - - if gallery_conf.get('find_mayavi_figures', False): - from mayavi import mlab - e = mlab.get_engine() - last_matplotlib_fig_num = fig_count + len(figure_list) - total_fig_num = last_matplotlib_fig_num + len(e.scenes) - mayavi_fig_nums = range(last_matplotlib_fig_num + 1, total_fig_num + 1) - - for scene, mayavi_fig_num in zip(e.scenes, mayavi_fig_nums): - current_fig = image_path.format(mayavi_fig_num) - mlab.savefig(current_fig, figure=scene) - # make sure the image is not too large - scale_image(current_fig, current_fig, 850, 999) - figure_list.append(current_fig) - mlab.close(all=True) - - return figure_rst(figure_list, gallery_conf['src_dir']) - - -def figure_rst(figure_list, sources_dir): - """Given a list of paths to figures generate the corresponding rst - - Depending on whether we have one or more figures, we use a - single rst call to 'image' or a horizontal list. - - Parameters - ---------- - figure_list : list of str - Strings are the figures' absolute paths - sources_dir : str - absolute path of Sphinx documentation sources - - Returns - ------- - images_rst : str - rst code to embed the images in the document - fig_num : int - number of figures saved - """ - - figure_paths = [os.path.relpath(figure_path, sources_dir) - .replace(os.sep, '/').lstrip('/') - for figure_path in figure_list] - images_rst = "" - if len(figure_paths) == 1: - figure_name = figure_paths[0] - images_rst = SINGLE_IMAGE % figure_name - elif len(figure_paths) > 1: - images_rst = HLIST_HEADER - for figure_name in figure_paths: - images_rst += HLIST_IMAGE_TEMPLATE % figure_name - - return images_rst, len(figure_list) - - -def scale_image(in_fname, out_fname, max_width, max_height): - """Scales an image with the same aspect ratio centered in an - image with a given max_width and max_height - if in_fname == out_fname the image can only be scaled down - """ - # local import to avoid testing dependency on PIL: - try: - from PIL import Image - except ImportError: - import Image - img = Image.open(in_fname) - width_in, height_in = img.size - scale_w = max_width / float(width_in) - scale_h = max_height / float(height_in) - - if height_in * scale_w <= max_height: - scale = scale_w - else: - scale = scale_h - - if scale >= 1.0 and in_fname == out_fname: - return - - width_sc = int(round(scale * width_in)) - height_sc = int(round(scale * height_in)) - - # resize the image - img.thumbnail((width_sc, height_sc), Image.ANTIALIAS) - - # insert centered - thumb = Image.new('RGB', (max_width, max_height), (255, 255, 255)) - pos_insert = ((max_width - width_sc) // 2, (max_height - height_sc) // 2) - thumb.paste(img, pos_insert) - - thumb.save(out_fname) - # Use optipng to perform lossless compression on the resized image if - # software is installed - if os.environ.get('SKLEARN_DOC_OPTIPNG', False): - try: - subprocess.call(["optipng", "-quiet", "-o", "9", out_fname]) - except Exception: - warnings.warn('Install optipng to reduce the size of the \ - generated images') - - -def save_thumbnail(image_path_template, src_file, gallery_conf): - """Save the thumbnail image""" - # read specification of the figure to display as thumbnail from main text - _, content = get_docstring_and_rest(src_file) - thumbnail_number = extract_thumbnail_number(content) - thumbnail_image_path = image_path_template.format(thumbnail_number) - - thumb_dir = os.path.join(os.path.dirname(thumbnail_image_path), 'thumb') - if not os.path.exists(thumb_dir): - os.makedirs(thumb_dir) - - base_image_name = os.path.splitext(os.path.basename(src_file))[0] - thumb_file = os.path.join(thumb_dir, - 'sphx_glr_%s_thumb.png' % base_image_name) - - if src_file in gallery_conf['failing_examples']: - broken_img = os.path.join(glr_path_static(), 'broken_example.png') - scale_image(broken_img, thumb_file, 200, 140) - - elif os.path.exists(thumbnail_image_path): - scale_image(thumbnail_image_path, thumb_file, 400, 280) - - elif not os.path.exists(thumb_file): - # create something to replace the thumbnail - default_thumb_file = os.path.join(glr_path_static(), 'no_image.png') - default_thumb_file = gallery_conf.get("default_thumb_file", - default_thumb_file) - scale_image(default_thumb_file, thumb_file, 200, 140) - - -def generate_dir_rst(src_dir, target_dir, gallery_conf, seen_backrefs): - """Generate the gallery reStructuredText for an example directory""" - if not os.path.exists(os.path.join(src_dir, 'README.txt')): - print(80 * '_') - print('Example directory %s does not have a README.txt file' % - src_dir) - print('Skipping this directory') - print(80 * '_') - return "", [] # because string is an expected return type - - with open(os.path.join(src_dir, 'README.txt')) as fid: - fhindex = fid.read() - # Add empty lines to avoid bug in issue #165 - fhindex += "\n\n" - - if not os.path.exists(target_dir): - os.makedirs(target_dir) - sorted_listdir = [fname for fname in sorted(os.listdir(src_dir)) - if fname.endswith('.py')] - entries_text = [] - computation_times = [] - build_target_dir = os.path.relpath(target_dir, gallery_conf['src_dir']) - for fname in sorted_listdir: - amount_of_code, time_elapsed = \ - generate_file_rst(fname, target_dir, src_dir, gallery_conf) - computation_times.append((time_elapsed, fname)) - new_fname = os.path.join(src_dir, fname) - intro = extract_intro(new_fname) - this_entry = _thumbnail_div(build_target_dir, fname, intro) + """ - -.. toctree:: - :hidden: - - /%s\n""" % os.path.join(build_target_dir, fname[:-3]).replace(os.sep, '/') - entries_text.append((amount_of_code, this_entry)) - - if gallery_conf['backreferences_dir']: - write_backreferences(seen_backrefs, gallery_conf, - target_dir, fname, intro) - - # sort to have the smallest entries in the beginning - entries_text.sort() - - for _, entry_text in entries_text: - fhindex += entry_text - - # clear at the end of the section - fhindex += """.. raw:: html\n -
\n\n""" - - return fhindex, computation_times - - -def execute_code_block(code_block, example_globals, - block_vars, gallery_conf): - """Executes the code block of the example file""" - time_elapsed = 0 - stdout = '' - - # If example is not suitable to run, skip executing its blocks - if not block_vars['execute_script']: - return stdout, time_elapsed - - plt.close('all') - cwd = os.getcwd() - # Redirect output to stdout and - orig_stdout = sys.stdout - src_file = block_vars['src_file'] - - try: - # First cd in the original example dir, so that any file - # created by the example get created in this directory - os.chdir(os.path.dirname(src_file)) - my_buffer = MixedEncodingStringIO() - my_stdout = Tee(sys.stdout, my_buffer) - sys.stdout = my_stdout - - t_start = time() - # don't use unicode_literals at the top of this file or you get - # nasty errors here on Py2.7 - exec(code_block, example_globals) - time_elapsed = time() - t_start - - sys.stdout = orig_stdout - - my_stdout = my_buffer.getvalue().strip().expandtabs() - # raise RuntimeError - if my_stdout: - stdout = CODE_OUTPUT.format(indent(my_stdout, u' ' * 4)) - os.chdir(cwd) - images_rst, fig_num = save_figures(block_vars['image_path'], - block_vars['fig_count'], gallery_conf) - - except Exception: - formatted_exception = traceback.format_exc() - - fail_example_warning = 80 * '_' + '\n' + \ - '%s failed to execute correctly:' % src_file + \ - formatted_exception + 80 * '_' + '\n' - warnings.warn(fail_example_warning) - - fig_num = 0 - images_rst = codestr2rst(formatted_exception, lang='pytb') - - # Breaks build on first example error - # XXX This check can break during testing e.g. if you uncomment the - # `raise RuntimeError` by the `my_stdout` call, maybe use `.get()`? - if gallery_conf['abort_on_example_error']: - raise - # Stores failing file - gallery_conf['failing_examples'][src_file] = formatted_exception - block_vars['execute_script'] = False - - finally: - os.chdir(cwd) - sys.stdout = orig_stdout - - code_output = u"\n{0}\n\n{1}\n\n".format(images_rst, stdout) - block_vars['fig_count'] += fig_num - - return code_output, time_elapsed - - -def clean_modules(): - """Remove "unload" seaborn from the name space - - After a script is executed it can load a variety of setting that one - does not want to influence in other examples in the gallery.""" - - # Horrible code to 'unload' seaborn, so that it resets - # its default when is load - # Python does not support unloading of modules - # https://bugs.python.org/issue9072 - for module in list(sys.modules.keys()): - if 'seaborn' in module: - del sys.modules[module] - - # Reset Matplotlib to default - plt.rcdefaults() - - -def generate_file_rst(fname, target_dir, src_dir, gallery_conf): - """Generate the rst file for a given example. - - Returns - ------- - amount_of_code : int - character count of the corresponding python script in file - time_elapsed : float - seconds required to run the script - """ - - src_file = os.path.normpath(os.path.join(src_dir, fname)) - example_file = os.path.join(target_dir, fname) - shutil.copyfile(src_file, example_file) - script_blocks = split_code_and_text_blocks(src_file) - amount_of_code = sum([len(bcontent) - for blabel, bcontent in script_blocks - if blabel == 'code']) - - if md5sum_is_current(example_file): - return amount_of_code, 0 - - image_dir = os.path.join(target_dir, 'images') - if not os.path.exists(image_dir): - os.makedirs(image_dir) - - base_image_name = os.path.splitext(fname)[0] - image_fname = 'sphx_glr_' + base_image_name + '_{0:03}.png' - build_image_dir = os.path.relpath(image_dir, gallery_conf['src_dir']) - image_path_template = os.path.join(image_dir, image_fname) - - ref_fname = os.path.relpath(example_file, gallery_conf['src_dir']) - ref_fname = ref_fname.replace(os.path.sep, '_') - example_rst = """\n\n.. _sphx_glr_{0}:\n\n""".format(ref_fname) - - filename_pattern = gallery_conf.get('filename_pattern') - execute_script = re.search(filename_pattern, src_file) and gallery_conf[ - 'plot_gallery'] - example_globals = { - # A lot of examples contains 'print(__doc__)' for example in - # scikit-learn so that running the example prints some useful - # information. Because the docstring has been separated from - # the code blocks in sphinx-gallery, __doc__ is actually - # __builtin__.__doc__ in the execution context and we do not - # want to print it - '__doc__': '', - # Examples may contain if __name__ == '__main__' guards - # for in example scikit-learn if the example uses multiprocessing - '__name__': '__main__', - # Don't ever support __file__: Issues #166 #212 - } - - # A simple example has two blocks: one for the - # example introduction/explanation and one for the code - is_example_notebook_like = len(script_blocks) > 2 - time_elapsed = 0 - block_vars = {'execute_script': execute_script, 'fig_count': 0, - 'image_path': image_path_template, 'src_file': src_file} - if block_vars['execute_script']: - print('Executing file %s' % src_file) - for blabel, bcontent in script_blocks: - if blabel == 'code': - code_output, rtime = execute_code_block(bcontent, - example_globals, - block_vars, - gallery_conf) - - time_elapsed += rtime - - if is_example_notebook_like: - example_rst += codestr2rst(bcontent) + '\n' - example_rst += code_output - else: - example_rst += code_output - if 'sphx-glr-script-out' in code_output: - # Add some vertical space after output - example_rst += "\n\n|\n\n" - example_rst += codestr2rst(bcontent) + '\n' - - else: - example_rst += bcontent + '\n\n' - - clean_modules() - - # Writes md5 checksum if example has build correctly - # not failed and was initially meant to run(no-plot shall not cache md5sum) - if block_vars['execute_script']: - with open(example_file + '.md5', 'w') as file_checksum: - file_checksum.write(get_md5sum(example_file)) - - save_thumbnail(image_path_template, src_file, gallery_conf) - - time_m, time_s = divmod(time_elapsed, 60) - example_nb = jupyter_notebook(script_blocks) - save_notebook(example_nb, example_file.replace('.py', '.ipynb')) - with codecs.open(os.path.join(target_dir, base_image_name + '.rst'), - mode='w', encoding='utf-8') as f: - example_rst += "**Total running time of the script:**" \ - " ({0: .0f} minutes {1: .3f} seconds)\n\n".format( - time_m, time_s) - example_rst += CODE_DOWNLOAD.format(fname, - fname.replace('.py', '.ipynb')) - example_rst += SPHX_GLR_SIG - f.write(example_rst) - - if block_vars['execute_script']: - print("{0} ran in : {1:.2g} seconds\n".format(src_file, time_elapsed)) - - return amount_of_code, time_elapsed diff --git a/doc/sphinxext/sphinx_gallery/notebook.py b/doc/sphinxext/sphinx_gallery/notebook.py deleted file mode 100644 index a0cfdbd7881d6..0000000000000 --- a/doc/sphinxext/sphinx_gallery/notebook.py +++ /dev/null @@ -1,193 +0,0 @@ -# -*- coding: utf-8 -*- -r""" -Parser for Jupyter notebooks -============================ - -Class that holds the Jupyter notebook information - -""" -# Author: Óscar Nájera -# License: 3-clause BSD - -from __future__ import division, absolute_import, print_function -from functools import partial -import argparse -import json -import re -import sys -from .py_source_parser import split_code_and_text_blocks - - -def jupyter_notebook_skeleton(): - """Returns a dictionary with the elements of a Jupyter notebook""" - py_version = sys.version_info - notebook_skeleton = { - "cells": [], - "metadata": { - "kernelspec": { - "display_name": "Python " + str(py_version[0]), - "language": "python", - "name": "python" + str(py_version[0]) - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": py_version[0] - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython" + str(py_version[0]), - "version": '{0}.{1}.{2}'.format(*sys.version_info[:3]) - } - }, - "nbformat": 4, - "nbformat_minor": 0 - } - return notebook_skeleton - - -def directive_fun(match, directive): - """Helper to fill in directives""" - directive_to_alert = dict(note="info", warning="danger") - return ('

{1}

{2}

' - .format(directive_to_alert[directive], directive.capitalize(), - match.group(1).strip())) - - -def rst2md(text): - """Converts the RST text from the examples docstrigs and comments - into markdown text for the Jupyter notebooks""" - - top_heading = re.compile(r'^=+$\s^([\w\s-]+)^=+$', flags=re.M) - text = re.sub(top_heading, r'# \1', text) - - math_eq = re.compile(r'^\.\. math::((?:.+)?(?:\n+^ .+)*)', flags=re.M) - text = re.sub(math_eq, - lambda match: r'\begin{{align}}{0}\end{{align}}'.format( - match.group(1).strip()), - text) - inline_math = re.compile(r':math:`(.+?)`', re.DOTALL) - text = re.sub(inline_math, r'$\1$', text) - - directives = ('warning', 'note') - for directive in directives: - directive_re = re.compile(r'^\.\. %s::((?:.+)?(?:\n+^ .+)*)' - % directive, flags=re.M) - text = re.sub(directive_re, - partial(directive_fun, directive=directive), text) - - links = re.compile(r'^ *\.\. _.*:.*$\n', flags=re.M) - text = re.sub(links, '', text) - - refs = re.compile(r':ref:`') - text = re.sub(refs, '`', text) - - contents = re.compile(r'^\s*\.\. contents::.*$(\n +:\S+: *$)*\n', - flags=re.M) - text = re.sub(contents, '', text) - - images = re.compile( - r'^\.\. image::(.*$)(?:\n *:alt:(.*$)\n)?(?: +:\S+:.*$\n)*', - flags=re.M) - text = re.sub( - images, lambda match: '![{1}]({0})\n'.format( - match.group(1).strip(), (match.group(2) or '').strip()), text) - - return text - - -def jupyter_notebook(script_blocks): - """Generate a Jupyter notebook file cell-by-cell - - Parameters - ---------- - script_blocks: list - script execution cells - """ - - work_notebook = jupyter_notebook_skeleton() - add_code_cell(work_notebook, "%matplotlib inline") - fill_notebook(work_notebook, script_blocks) - - return work_notebook - - -def add_code_cell(work_notebook, code): - """Add a code cell to the notebook - - Parameters - ---------- - code : str - Cell content - """ - - code_cell = { - "cell_type": "code", - "execution_count": None, - "metadata": {"collapsed": False}, - "outputs": [], - "source": [code.strip()] - } - work_notebook["cells"].append(code_cell) - - -def add_markdown_cell(work_notebook, text): - """Add a markdown cell to the notebook - - Parameters - ---------- - code : str - Cell content - """ - markdown_cell = { - "cell_type": "markdown", - "metadata": {}, - "source": [rst2md(text)] - } - work_notebook["cells"].append(markdown_cell) - - -def fill_notebook(work_notebook, script_blocks): - """Writes the Jupyter notebook cells - - Parameters - ---------- - script_blocks : list of tuples - """ - - for blabel, bcontent in script_blocks: - if blabel == 'code': - add_code_cell(work_notebook, bcontent) - else: - add_markdown_cell(work_notebook, bcontent + '\n') - - -def save_notebook(work_notebook, write_file): - """Saves the Jupyter work_notebook to write_file""" - with open(write_file, 'w') as out_nb: - json.dump(work_notebook, out_nb, indent=2) - - -############################################################################### -# Notebook shell utility - -def python_to_jupyter_cli(args=None, namespace=None): - """Exposes the jupyter notebook renderer to the command line - - Takes the same arguments as ArgumentParser.parse_args - """ - parser = argparse.ArgumentParser( - description='Sphinx-Gallery Notebook converter') - parser.add_argument('python_src_file', nargs='+', - help='Input Python file script to convert. ' - 'Supports multiple files and shell wildcards' - ' (e.g. *.py)') - args = parser.parse_args(args, namespace) - - for src_file in args.python_src_file: - blocks = split_code_and_text_blocks(src_file) - print('Converting {0}'.format(src_file)) - example_nb = jupyter_notebook(blocks) - save_notebook(example_nb, src_file.replace('.py', '.ipynb')) diff --git a/doc/sphinxext/sphinx_gallery/py_source_parser.py b/doc/sphinxext/sphinx_gallery/py_source_parser.py deleted file mode 100644 index d397087f99fbd..0000000000000 --- a/doc/sphinxext/sphinx_gallery/py_source_parser.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -r""" -Parser for python source files -============================== -""" -# Created Sun Nov 27 14:03:07 2016 -# Author: Óscar Nájera - -from __future__ import division, absolute_import, print_function -import ast -import re -from textwrap import dedent - -SYNTAX_ERROR_DOCSTRING = """ -SyntaxError -=========== - -Example script with invalid Python syntax -""" - - -def get_docstring_and_rest(filename): - """Separate `filename` content between docstring and the rest - - Strongly inspired from ast.get_docstring. - - Returns - ------- - docstring: str - docstring of `filename` - rest: str - `filename` content without the docstring - """ - # can't use codecs.open(filename, 'r', 'utf-8') here b/c ast doesn't - # seem to work with unicode strings in Python2.7 - # "SyntaxError: encoding declaration in Unicode string" - with open(filename, 'rb') as fid: - content = fid.read() - # change from Windows format to UNIX for uniformity - content = content.replace(b'\r\n', b'\n') - - try: - node = ast.parse(content) - except SyntaxError: - return SYNTAX_ERROR_DOCSTRING, content.decode('utf-8') - - if not isinstance(node, ast.Module): - raise TypeError("This function only supports modules. " - "You provided {0}".format(node.__class__.__name__)) - if node.body and isinstance(node.body[0], ast.Expr) and \ - isinstance(node.body[0].value, ast.Str): - docstring_node = node.body[0] - docstring = docstring_node.value.s - if hasattr(docstring, 'decode'): # python2.7 - docstring = docstring.decode('utf-8') - # This get the content of the file after the docstring last line - # Note: 'maxsplit' argument is not a keyword argument in python2 - rest = content.decode('utf-8').split('\n', docstring_node.lineno)[-1] - return docstring, rest - else: - raise ValueError(('Could not find docstring in file "{0}". ' - 'A docstring is required by sphinx-gallery') - .format(filename)) - - -def split_code_and_text_blocks(source_file): - """Return list with source file separated into code and text blocks. - - Returns - ------- - blocks : list of (label, content) - List where each element is a tuple with the label ('text' or 'code'), - and content string of block. - """ - docstring, rest_of_content = get_docstring_and_rest(source_file) - blocks = [('text', docstring)] - - pattern = re.compile( - r'(?P^#{20,}.*)\s(?P(?:^#.*\s)*)', - flags=re.M) - - pos_so_far = 0 - for match in re.finditer(pattern, rest_of_content): - match_start_pos, match_end_pos = match.span() - code_block_content = rest_of_content[pos_so_far:match_start_pos] - text_content = match.group('text_content') - sub_pat = re.compile('^#', flags=re.M) - text_block_content = dedent(re.sub(sub_pat, '', text_content)).lstrip() - if code_block_content.strip(): - blocks.append(('code', code_block_content)) - if text_block_content.strip(): - blocks.append(('text', text_block_content)) - pos_so_far = match_end_pos - - remaining_content = rest_of_content[pos_so_far:] - if remaining_content.strip(): - blocks.append(('code', remaining_content)) - - return blocks From f622c7ed1c92ff7918a318ecef64b45b70952f2a Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Wed, 19 Jul 2017 13:54:31 +1000 Subject: [PATCH 2/3] [doc build] --- build_tools/circle/build_doc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_tools/circle/build_doc.sh b/build_tools/circle/build_doc.sh index 8788f459769c5..b3f785254c2ae 100755 --- a/build_tools/circle/build_doc.sh +++ b/build_tools/circle/build_doc.sh @@ -109,7 +109,7 @@ conda update --yes --quiet conda conda create -n $CONDA_ENV_NAME --yes --quiet python numpy scipy \ cython nose coverage matplotlib sphinx=1.6.2 pillow source activate testenv -pip install numpydoc sphinx-gallery +pip install sphinx-gallery numpydoc # Build and install scikit-learn in dev mode python setup.py develop From 34790b5315118e40394516d428ae972d80664567 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Fri, 8 Sep 2017 11:58:23 -0400 Subject: [PATCH 3/3] add dependencies for building the docs to readme --- doc/README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/README.md b/doc/README.md index 141db3d7a8da5..82240fb701aa3 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,8 +1,13 @@ # Documentation for scikit-learn This section contains the full manual and web page as displayed in -http://scikit-learn.org. To generate the full web page, including -the example gallery (this might take a while): +http://scikit-learn.org. +Building the website requires the sphinx and sphinx-gallery packages: + + pip install sphinx sphinx-gallery + +To generate the full web page, including the example gallery (this might take a +while): make html @@ -16,7 +21,6 @@ To build the PDF manual, run make latexpdf - The website is hosted at github and can be updated manually (for releases) by pushing to the https://github.com/scikit-learn/scikit-learn.github.io repository.