From 52b0e16a787f29f15ef673c36496fb7cff866f5c Mon Sep 17 00:00:00 2001 From: emmanuelle Date: Tue, 10 May 2016 20:53:00 +0200 Subject: [PATCH] Included sphinx-gallery files I had to make a slightly modification in one of sphinx-gallery's files to make it work with scikit-image. I opened a pull request on sphinx-gallery github, hopefully we can delete the local copy after their next release. --- doc/ext/sphinx_gallery/__init__.py | 13 + .../sphinx_gallery/_static/broken_example.png | Bin 0 -> 21404 bytes .../sphinx_gallery/_static/broken_stamp.svg | 90 +++ doc/ext/sphinx_gallery/_static/gallery.css | 127 ++++ doc/ext/sphinx_gallery/_static/no_image.png | Bin 0 -> 4315 bytes doc/ext/sphinx_gallery/backreferences.py | 180 +++++ doc/ext/sphinx_gallery/docs_resolv.py | 436 ++++++++++++ doc/ext/sphinx_gallery/gen_gallery.py | 170 +++++ doc/ext/sphinx_gallery/gen_rst.py | 656 ++++++++++++++++++ doc/ext/sphinx_gallery/notebook.py | 123 ++++ 10 files changed, 1795 insertions(+) create mode 100644 doc/ext/sphinx_gallery/__init__.py create mode 100644 doc/ext/sphinx_gallery/_static/broken_example.png create mode 100644 doc/ext/sphinx_gallery/_static/broken_stamp.svg create mode 100644 doc/ext/sphinx_gallery/_static/gallery.css create mode 100644 doc/ext/sphinx_gallery/_static/no_image.png create mode 100644 doc/ext/sphinx_gallery/backreferences.py create mode 100644 doc/ext/sphinx_gallery/docs_resolv.py create mode 100644 doc/ext/sphinx_gallery/gen_gallery.py create mode 100644 doc/ext/sphinx_gallery/gen_rst.py create mode 100644 doc/ext/sphinx_gallery/notebook.py diff --git a/doc/ext/sphinx_gallery/__init__.py b/doc/ext/sphinx_gallery/__init__.py new file mode 100644 index 00000000..c5ff4151 --- /dev/null +++ b/doc/ext/sphinx_gallery/__init__.py @@ -0,0 +1,13 @@ +""" +============== +Sphinx Gallery +============== + +""" +import os +__version__ = '0.1.2' + + +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/ext/sphinx_gallery/_static/broken_example.png b/doc/ext/sphinx_gallery/_static/broken_example.png new file mode 100644 index 0000000000000000000000000000000000000000..4fea24e7df4781c2c32c8d7995511ac89e953145 GIT binary patch literal 21404 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 + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/doc/ext/sphinx_gallery/_static/gallery.css b/doc/ext/sphinx_gallery/_static/gallery.css new file mode 100644 index 00000000..dd599b40 --- /dev/null +++ b/doc/ext/sphinx_gallery/_static/gallery.css @@ -0,0 +1,127 @@ +/* +Sphinx-Gallery is has compatible CSS to fix default sphinx themes +Tested for Sphinx 1.3.1 for all themes: default, alabaster, sphinxdoc, +scrolls, agogo, traditional, nature, haiku, pyramid +Tested for Read the docs theme 0.1.7 */ +.sphx-glr-thumbcontainer { + background: #fff; + border: solid #fff 1px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; + box-shadow: none; + float: left; + margin: 5px; + min-height: 230px; + padding-top: 5px; + position: relative; +} +.sphx-glr-thumbcontainer:hover { + border: solid #b4ddfc 1px; + box-shadow: 0 0 15px rgba(142, 176, 202, 0.5); +} +.sphx-glr-thumbcontainer a.internal { + bottom: 0; + display: block; + left: 0; + padding: 150px 10px 0; + position: absolute; + right: 0; + top: 0; +} +/* Next one is to avoid Sphinx traditional theme to cover all the +thumbnail with its default link Background color */ +.sphx-glr-thumbcontainer a.internal:hover { + background-color: transparent; +} + +.sphx-glr-thumbcontainer p { + margin: 0 0 .1em 0; +} +.sphx-glr-thumbcontainer .figure { + margin: 10px; + width: 160px; +} +.sphx-glr-thumbcontainer img { + display: inline; + max-height: 160px; + width: 160px; +} +.sphx-glr-thumbcontainer[tooltip]:hover:after { + background: rgba(0, 0, 0, 0.8); + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + color: #fff; + content: attr(tooltip); + left: 95%; + padding: 5px 15px; + position: absolute; + z-index: 98; + width: 220px; + bottom: 52%; +} +.sphx-glr-thumbcontainer[tooltip]:hover:before { + border: solid; + border-color: #333 transparent; + border-width: 18px 0 0 20px; + bottom: 58%; + content: ''; + left: 85%; + position: absolute; + z-index: 99; +} + +.highlight-pytb pre { + background-color: #ffe4e4; + border: 1px solid #f66; + margin-top: 10px; + padding: 7px; +} + +.sphx-glr-script-out { + color: #888; + margin: 0; +} +.sphx-glr-script-out .highlight { + background-color: transparent; + margin-left: 2.5em; + margin-top: -1.4em; +} +.sphx-glr-script-out .highlight pre { + background-color: #fafae2; + border: 0; + max-height: 30em; + overflow: auto; + padding-left: 1ex; + margin: 0px; + word-break: break-word; +} +.sphx-glr-script-out + p { + margin-top: 1.8em; +} +blockquote.sphx-glr-script-out { + margin-left: 0pt; +} +.sphx-glr-download { + background-color: #ffc; + border: 1px solid #c2c22d; + border-radius: 4px; + margin: 1em auto 1ex auto; + max-width: 45ex; + padding: 1ex; +} +.sphx-glr-download a { + color: #4b4600; +} + +ul.sphx-glr-horizontal { + list-style: none; + padding: 0; +} +ul.sphx-glr-horizontal li { + display: inline; +} +ul.sphx-glr-horizontal img { + height: auto !important; +} diff --git a/doc/ext/sphinx_gallery/_static/no_image.png b/doc/ext/sphinx_gallery/_static/no_image.png new file mode 100644 index 0000000000000000000000000000000000000000..8c2d48d5d3f0044cf0399b3608d8eb5db6f9d12d GIT binary patch literal 4315 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 HW8D7$6^1w` literal 0 HcmV?d00001 diff --git a/doc/ext/sphinx_gallery/backreferences.py b/doc/ext/sphinx_gallery/backreferences.py new file mode 100644 index 00000000..4fe579c6 --- /dev/null +++ b/doc/ext/sphinx_gallery/backreferences.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# Author: Óscar Nájera +# License: 3-clause BSD +""" +======================== +Backreferences Generator +======================== + +Reviews generated example files in order to keep track of used modules +""" + +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() + finder.visit(ast.parse(code)) + + 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) + module, attribute = full_name.rsplit('.', 1) + # 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]) + 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""" + example_file = os.path.join(target_dir, fname) + backrefs = scan_used_functions(example_file, gallery_conf) + for backref in backrefs: + include_path = os.path.join(gallery_conf['mod_example_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(target_dir, fname, snippet, + is_backref=True)) + seen_backrefs.add(backref) diff --git a/doc/ext/sphinx_gallery/docs_resolv.py b/doc/ext/sphinx_gallery/docs_resolv.py new file mode 100644 index 00000000..0675a131 --- /dev/null +++ b/doc/ext/sphinx_gallery/docs_resolv.py @@ -0,0 +1,436 @@ +# -*- coding: utf-8 -*- +# Author: Óscar Nájera +# License: 3-clause BSD +############################################################################### +# Documentation link resolver objects +from __future__ import print_function +import gzip +import os +import posixpath +import re +import shelve +import sys + +# 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] + '.html' + + 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 + + # 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. + cobi['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 = {} + + for this_module, url in gallery_conf['reference_url'].items(): + try: + if url is None: + doc_resolvers[this_module] = SphinxDocLinkResolver( + app.builder.outdir, + gallery_dir, + relative=True) + else: + doc_resolvers[this_module] = SphinxDocLinkResolver(url, + 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 = '.' + + for dirpath, _, filenames in os.walk(html_gallery_dir): + for fname in filenames: + print('\tprocessing: %s' % fname) + full_fname = os.path.join(html_gallery_dir, dirpath, fname) + subpath = dirpath[len(html_gallery_dir) + 1:] + pickle_fname = os.path.join(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: + print("The following error has occurred:\n") + print(repr(e)) + continue + + if link is not None: + parts = name.split('.') + name_html = period.join(orig_pattern % part + for part in parts) + str_repl[name_html] = link_pattern % (link, name_html) + # do the replacement in the html file + + # ensure greediness + names = sorted(str_repl, key=len, reverse=True) + expr = re.compile(r'(? 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 = expr.sub(substitute_link, line) + fid.write(line.encode('utf-8')) + print('[done]') + + +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/ext/sphinx_gallery/gen_gallery.py b/doc/ext/sphinx_gallery/gen_gallery.py new file mode 100644 index 00000000..3da9a59a --- /dev/null +++ b/doc/ext/sphinx_gallery/gen_gallery.py @@ -0,0 +1,170 @@ +# -*- 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 re +import os +from . import glr_path_static +from .gen_rst import generate_dir_rst +from .docs_resolv import embed_code_links + + +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 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 + """ + try: + plot_gallery = eval(app.builder.config.plot_gallery) + except TypeError: + plot_gallery = bool(app.builder.config.plot_gallery) + + 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) + + # 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()) + + clean_gallery_out(app.builder.outdir) + + 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] + + mod_examples_dir = os.path.relpath(gallery_conf['mod_example_dir'], + app.builder.srcdir) + seen_backrefs = set() + + computation_times = [] + + # cd to the appropriate directory regardless of sphinx configuration + working_dir = os.getcwd() + os.chdir(app.builder.srcdir) + for examples_dir, gallery_dir in zip(examples_dirs, gallery_dirs): + examples_dir = os.path.relpath(examples_dir, + app.builder.srcdir) + gallery_dir = os.path.relpath(gallery_dir, + app.builder.srcdir) + + for workdir in [examples_dir, gallery_dir, mod_examples_dir]: + if not os.path.exists(workdir): + os.makedirs(workdir) + # we create an index.rst with all examples + fhindex = open(os.path.join(gallery_dir, 'index.rst'), 'w') + # 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) + + computation_times += this_computation_times + + fhindex.write(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 + + fhindex.flush() + + # Back to initial directory + os.chdir(working_dir) + + 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""" + + examples_path = os.path.join(app.srcdir, + app.config.sphinx_gallery_conf["mod_example_dir"], + "%s.examples" % name) + + if not os.path.exists(examples_path): + # touch file + open(examples_path, 'w').close() + + +gallery_conf = { + 'filename_pattern': re.escape(os.sep) + 'plot', + 'examples_dirs': '../examples', + 'gallery_dirs': 'auto_examples', + 'mod_example_dir': os.path.join('modules', 'generated'), + 'doc_module': (), + 'reference_url': {}, +} + + +def setup(app): + """Setup sphinx-gallery sphinx extension""" + app.add_config_value('plot_gallery', True, 'html') + app.add_config_value('abort_on_example_error', False, 'html') + app.add_config_value('sphinx_gallery_conf', gallery_conf, 'html') + app.add_stylesheet('gallery.css') + + if 'sphinx.ext.autodoc' in app._extensions: + app.connect('autodoc-process-docstring', touch_empty_backreferences) + + app.connect('builder-inited', generate_gallery_rst) + + app.connect('build-finished', embed_code_links) + + +def setup_module(): + # HACK: Stop nosetests running setup() above + pass diff --git a/doc/ext/sphinx_gallery/gen_rst.py b/doc/ext/sphinx_gallery/gen_rst.py new file mode 100644 index 00000000..318bc60f --- /dev/null +++ b/doc/ext/sphinx_gallery/gen_rst.py @@ -0,0 +1,656 @@ +# -*- 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 ast +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 +from textwrap import dedent +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 + +try: + # make sure that the Agg backend is set before importing any + # matplotlib + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt +except ImportError: + # this script can be imported by nosetest to find tests to run: we should + # not impose the matplotlib requirement in that case. + pass + +from . import glr_path_static +from .backreferences import write_backreferences, _thumbnail_div +from .notebook import 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() + + +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) + + +############################################################################### +CODE_DOWNLOAD = """**Total running time of the script:** +({0:.0f} minutes {1:.3f} seconds)\n\n +\n.. container:: sphx-glr-download + + **Download Python source code:** :download:`{2} <{2}>`\n +\n.. container:: sphx-glr-download + + **Download IPython notebook:** :download:`{3} <{3}>`\n""" + +# 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""" + + +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 f: + content = f.read() + + node = ast.parse(content) + 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 + + +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 text2string(content): + """Returns a string without the extra triple quotes""" + try: + return ast.literal_eval(content) + '\n' + except Exception: + return content + '\n' + + +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("^\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, 'r') as src_data: + src_content = src_data.read() + + # data needs to be encoded in python3 before hashing + if sys.version_info[0] == 3: + src_content = src_content.encode('utf-8') + + src_md5 = hashlib.md5(src_content).hexdigest() + return src_md5 + + +def check_md5sum_change(src_file): + """Returns True if src_file has a different md5sum""" + + src_md5 = get_md5sum(src_file) + + src_md5_file = src_file + '.md5' + src_file_changed = True + if os.path.exists(src_md5_file): + with open(src_md5_file, 'r') as file_checksum: + ref_md5 = file_checksum.read() + if src_md5 == ref_md5: + src_file_changed = False + + if src_file_changed: + with open(src_md5_file, 'w') as file_checksum: + file_checksum.write(src_md5) + + return src_file_changed + + +def _plots_are_current(src_file, image_path): + """Test existence of image file and no change in md5sum of + example""" + + has_image = os.path.exists(image_path) + src_file_changed = check_md5sum_change(src_file) + + return has_image and not src_file_changed + + +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 + + Returns + ------- + list of strings containing the full path to each figure + """ + figure_list = [] + + fig_managers = matplotlib._pylab_helpers.Gcf.get_all_fig_managers() + for fig_mngr in fig_managers: + # 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_mngr.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_mngr.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_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(thumbnail_image_path, base_image_name, gallery_conf): + """Save the thumbnail image""" + thumb_dir = os.path.join(os.path.dirname(thumbnail_image_path), 'thumb') + if not os.path.exists(thumb_dir): + os.makedirs(thumb_dir) + + thumb_file = os.path.join(thumb_dir, + 'sphx_glr_%s_thumb.png' % base_image_name) + + if 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 + + fhindex = open(os.path.join(src_dir, 'README.txt')).read() + 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 = [] + 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) + write_backreferences(seen_backrefs, gallery_conf, + target_dir, fname, intro) + this_entry = _thumbnail_div(target_dir, fname, intro) + """ + +.. toctree:: + :hidden: + + /%s/%s\n""" % (target_dir, fname[:-3]) + entries_text.append((amount_of_code, this_entry)) + + # 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_script(code_block, example_globals, image_path, fig_count, + src_file, gallery_conf): + """Executes the code block of the example file""" + time_elapsed = 0 + stdout = '' + + # We need to execute the code + print('plotting code blocks in %s' % src_file) + + plt.close('all') + cwd = os.getcwd() + # Redirect output to stdout and + orig_stdout = sys.stdout + + 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) + figure_list = save_figures(image_path, fig_count, gallery_conf) + + # Depending on whether we have one or more figures, we're using a + # horizontal list or a single rst call to 'image'. + image_list = "" + if len(figure_list) == 1: + figure_name = figure_list[0] + image_list = SINGLE_IMAGE % figure_name.lstrip('/') + elif len(figure_list) > 1: + image_list = HLIST_HEADER + for figure_name in figure_list: + image_list += HLIST_IMAGE_TEMPLATE % figure_name.lstrip('/') + + except Exception: + formatted_exception = traceback.format_exc() + + sys.stdout = orig_stdout # need this here so these lines don't bomb + print(80 * '_') + print('%s is not compiling:' % src_file) + print(formatted_exception) + print(80 * '_') + + figure_list = [] + image_list = codestr2rst(formatted_exception, lang='pytb') + + # Overrides the output thumbnail in the gallery for easy identification + broken_img = os.path.join(glr_path_static(), 'broken_example.png') + shutil.copyfile(broken_img, os.path.join(cwd, image_path.format(1))) + fig_count += 1 # raise count to avoid overwriting image + + # 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 + + finally: + os.chdir(cwd) + sys.stdout = orig_stdout + + print(" - time elapsed : %.2g sec" % time_elapsed) + code_output = u"\n{0}\n\n{1}\n\n".format(image_list, stdout) + + return code_output, time_elapsed, fig_count + len(figure_list) + + +def generate_file_rst(fname, target_dir, src_dir, gallery_conf): + """ Generate the rst file for a given example. + + Returns the amout of code (in characters) of the corresponding + files. + """ + + src_file = os.path.join(src_dir, fname) + example_file = os.path.join(target_dir, fname) + shutil.copyfile(src_file, example_file) + + 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' + image_path_template = os.path.join(image_dir, image_fname) + + script_blocks = split_code_and_text_blocks(example_file) + + # read specification of the figure to display as thumbnail from main text + _, content = get_docstring_and_rest(example_file) + thumbnail_number = extract_thumbnail_number(content) + + amount_of_code = sum([len(bcontent) + for blabel, bcontent in script_blocks + if blabel == 'code']) + + first_image_path = image_path_template.format(1) + if _plots_are_current(example_file, first_image_path): + return amount_of_code, 0 + + time_elapsed = 0 + + ref_fname = example_file.replace(os.path.sep, '_') + example_rst = """\n\n.. _sphx_glr_{0}:\n\n""".format(ref_fname) + example_nb = Notebook(fname, target_dir) + + filename_pattern = gallery_conf.get('filename_pattern') + if 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__'} + + fig_count = 0 + # 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 + for blabel, bcontent in script_blocks: + if blabel == 'code': + code_output, rtime, fig_count = execute_script(bcontent, + example_globals, + image_path_template, + fig_count, + src_file, + gallery_conf) + + time_elapsed += rtime + example_nb.add_code_cell(bcontent) + + 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 += text2string(bcontent) + '\n' + example_nb.add_markdown_cell(text2string(bcontent)) + else: + for blabel, bcontent in script_blocks: + if blabel == 'code': + example_rst += codestr2rst(bcontent) + '\n' + example_nb.add_code_cell(bcontent) + else: + example_rst += bcontent + '\n' + example_nb.add_markdown_cell(text2string(bcontent)) + + thumbnail_image_path = image_path_template.format(thumbnail_number) + save_thumbnail(thumbnail_image_path, base_image_name, gallery_conf) + + time_m, time_s = divmod(time_elapsed, 60) + example_nb.save_file() + with codecs.open(os.path.join(target_dir, base_image_name + '.rst'), + mode='w', encoding='utf-8') as f: + example_rst += CODE_DOWNLOAD.format(time_m, time_s, fname, + example_nb.file_name) + f.write(example_rst) + + return amount_of_code, time_elapsed diff --git a/doc/ext/sphinx_gallery/notebook.py b/doc/ext/sphinx_gallery/notebook.py new file mode 100644 index 00000000..c0ee5cd8 --- /dev/null +++ b/doc/ext/sphinx_gallery/notebook.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +r""" +============================ +Parser for Jupyter notebooks +============================ + +Class that holds the Ipython notebook information + +""" +# Author: Óscar Nájera +# License: 3-clause BSD + +from __future__ import division, absolute_import, print_function +import json +import os +import re +import sys + +def ipy_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 rst2md(text): + """Converts the RST text from the examples docstrigs and comments + into markdown text for the IPython 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'$${0}$$'.format(match.group(1).strip()), + text) + inline_math = re.compile(r':math:`(.+)`') + text = re.sub(inline_math, r'$\1$', text) + + return text + + +class Notebook(object): + """Ipython notebook object + + Constructs the file cell-by-cell and writes it at the end""" + + def __init__(self, file_name, target_dir): + """Declare the skeleton of the notebook + + Parameters + ---------- + file_name : str + original script file name, .py extension will be renamed + target_dir: str + directory where notebook file is to be saved + """ + + self.file_name = file_name.replace('.py', '.ipynb') + self.write_file = os.path.join(target_dir, self.file_name) + self.work_notebook = ipy_notebook_skeleton() + self.add_code_cell("%matplotlib inline") + + def add_code_cell(self, 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()] + } + self.work_notebook["cells"].append(code_cell) + + def add_markdown_cell(self, text): + """Add a markdown cell to the notebook + + Parameters + ---------- + code : str + Cell content + """ + markdown_cell = { + "cell_type": "markdown", + "metadata": {}, + "source": [rst2md(text)] + } + self.work_notebook["cells"].append(markdown_cell) + + def save_file(self): + """Saves the notebook to a file""" + with open(self.write_file, 'w') as out_nb: + json.dump(self.work_notebook, out_nb, indent=2)