-
Notifications
You must be signed in to change notification settings - Fork 207
MRG: Allow sorting subsection files #281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
30030bf
af055fc
5305f9f
c51c93e
7dba0b7
933d51c
2f97da7
e281409
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -153,22 +153,30 @@ def codestr2rst(codestr, lang='python', lineno=None): | |
return code_directive + indented_block | ||
|
||
|
||
def extract_intro(filename, docstring): | ||
def extract_intro_and_title(filename, docstring): | ||
""" Extract the first paragraph of module-level docstring. max:95 char""" | ||
|
||
# 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: | ||
# remove comments and other syntax like `.. _link:` | ||
paragraphs = [p for p in paragraphs if not p.startswith('.. ')] | ||
if len(paragraphs) <= 1: | ||
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)) | ||
# Title is the first paragraph with any ReSTructuredText title chars | ||
# removed, i.e. lines that consist of (all the same) 7-bit non-ASCII chars. | ||
# This conditional is not perfect but should hopefully be good enough. | ||
title = paragraphs[0].strip().split('\n') | ||
title = ' '.join(t for t in title if len(t) > 0 and | ||
(ord(t[0]) >= 128 or t[0].isalnum())) | ||
# Concatenate all lines of the first paragraph and truncate at 95 chars | ||
first_paragraph = re.sub('\n', ' ', paragraphs[1]) | ||
first_paragraph = (first_paragraph[:95] + '...' | ||
if len(first_paragraph) > 95 else first_paragraph) | ||
|
||
return first_paragraph | ||
return first_paragraph, title | ||
|
||
|
||
def get_md5sum(src_file): | ||
|
@@ -375,8 +383,10 @@ def generate_dir_rst(src_dir, target_dir, gallery_conf, seen_backrefs): | |
|
||
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')] | ||
listdir = [fname for fname in os.listdir(src_dir) | ||
if fname.endswith('.py')] | ||
sorted_listdir = sorted( | ||
listdir, key=gallery_conf['within_subsection_order'](src_dir)) | ||
entries_text = [] | ||
computation_times = [] | ||
build_target_dir = os.path.relpath(target_dir, gallery_conf['src_dir']) | ||
|
@@ -385,29 +395,25 @@ def generate_dir_rst(src_dir, target_dir, gallery_conf, seen_backrefs): | |
'Generating gallery for %s ' % build_target_dir, | ||
length=len(sorted_listdir)) | ||
for fname in iterator: | ||
intro, amount_of_code, time_elapsed = generate_file_rst( | ||
intro, time_elapsed = generate_file_rst( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that this is being factored out |
||
fname, | ||
target_dir, | ||
src_dir, | ||
gallery_conf) | ||
computation_times.append((time_elapsed, fname)) | ||
new_fname = os.path.join(src_dir, 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)) | ||
entries_text.append(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: | ||
for entry_text in entries_text: | ||
fhindex += entry_text | ||
|
||
# clear at the end of the section | ||
|
@@ -531,8 +537,6 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf): | |
------- | ||
intro: str | ||
The introduction of the example | ||
amount_of_code : int | ||
character count of the corresponding python script in file | ||
time_elapsed : float | ||
seconds required to run the script | ||
""" | ||
|
@@ -541,13 +545,10 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf): | |
example_file = os.path.join(target_dir, fname) | ||
shutil.copyfile(src_file, example_file) | ||
file_conf, script_blocks = split_code_and_text_blocks(src_file) | ||
amount_of_code = sum([len(bcontent) | ||
for blabel, bcontent, lineno in script_blocks | ||
if blabel == 'code']) | ||
intro = extract_intro(fname, script_blocks[0][1]) | ||
intro, title = extract_intro_and_title(fname, script_blocks[0][1]) | ||
|
||
if md5sum_is_current(example_file): | ||
return intro, amount_of_code, 0 | ||
return intro, 0 | ||
|
||
image_dir = os.path.join(target_dir, 'images') | ||
if not os.path.exists(image_dir): | ||
|
@@ -646,4 +647,4 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf): | |
if block_vars['execute_script']: | ||
logger.debug("%s ran in : %.2g seconds", src_file, time_elapsed) | ||
|
||
return intro, amount_of_code, time_elapsed | ||
return intro, time_elapsed |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
# -*- coding: utf-8 -*- | ||
r""" | ||
Sorters for Sphinx-Gallery subsections | ||
====================================== | ||
Sorters for Sphinx-Gallery (sub)sections | ||
======================================== | ||
|
||
Sorting key functions for gallery subsection folders | ||
Sorting key functions for gallery subsection folders and section files. | ||
""" | ||
# Created: Sun May 21 20:38:59 2017 | ||
# Author: Óscar Nájera | ||
|
@@ -13,21 +13,24 @@ | |
import os | ||
import types | ||
|
||
from .gen_rst import extract_intro_and_title | ||
from .py_source_parser import split_code_and_text_blocks | ||
|
||
|
||
class ExplicitOrder(object): | ||
"""Sorting key for all galleries subsections | ||
"""Sorting key for all gallery subsections. | ||
|
||
This requires all folders to be listed otherwise an exception is raised | ||
This requires all folders to be listed otherwise an exception is raised. | ||
|
||
Parameters | ||
---------- | ||
ordered_list : list, tuple, types.GeneratorType | ||
Hold the paths of each galleries' subsections | ||
Hold the paths of each galleries' subsections. | ||
|
||
Raises | ||
------ | ||
ValueError | ||
Wrong input type or Subgallery path missing | ||
Wrong input type or Subgallery path missing. | ||
""" | ||
|
||
def __init__(self, ordered_list): | ||
|
@@ -46,3 +49,71 @@ def __call__(self, item): | |
raise ValueError('If you use an explicit folder ordering, you ' | ||
'must specify all folders. Explicit order not ' | ||
'found for {}'.format(item)) | ||
|
||
|
||
class _SortKey(object): | ||
"""Base class for section order key classes.""" | ||
|
||
def __init__(self, src_dir): | ||
self.src_dir = src_dir | ||
|
||
|
||
class NumberOfCodeLinesSortKey(_SortKey): | ||
"""Sort examples in src_dir by the number of code lines. | ||
|
||
Parameters | ||
---------- | ||
src_dir : str | ||
The source directory. | ||
""" | ||
|
||
def __call__(self, filename): | ||
src_file = os.path.normpath(os.path.join(self.src_dir, filename)) | ||
file_conf, script_blocks = split_code_and_text_blocks(src_file) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah I see that it isn't strictly |
||
amount_of_code = sum([len(bcontent) | ||
for blabel, bcontent, lineno in script_blocks | ||
if blabel == 'code']) | ||
return amount_of_code | ||
|
||
|
||
class FileSizeSortKey(_SortKey): | ||
"""Sort examples in src_dir by file size. | ||
|
||
Parameters | ||
---------- | ||
src_dir : str | ||
The source directory. | ||
""" | ||
|
||
def __call__(self, filename): | ||
src_file = os.path.normpath(os.path.join(self.src_dir, filename)) | ||
return os.stat(src_file).st_size | ||
|
||
|
||
class FileNameSortKey(_SortKey): | ||
"""Sort examples in src_dir by file size. | ||
|
||
Parameters | ||
---------- | ||
src_dir : str | ||
The source directory. | ||
""" | ||
|
||
def __call__(self, filename): | ||
return filename | ||
|
||
|
||
class ExampleTitleSortKey(_SortKey): | ||
"""Sort examples in src_dir by example title. | ||
|
||
Parameters | ||
---------- | ||
src_dir : str | ||
The source directory. | ||
""" | ||
|
||
def __call__(self, filename): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this will break for many people's sphinx-gallery setups, as there are a few projects (ahem, MNE-python being one of them) that put sphinx tags just above their titles. I feel like I opened an issue about this a while back but the dev team wasn't keen on supporting that use-case... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yes I should probably parse the title however it's found and parsed elsewhere (and refactor to DRY) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually this already uses
And it passed, so we should be okay here. |
||
src_file = os.path.normpath(os.path.join(self.src_dir, filename)) | ||
_, script_blocks = split_code_and_text_blocks(src_file) | ||
_, title = extract_intro_and_title(src_file, script_blocks[0][1]) | ||
return title |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,15 +5,17 @@ | |
Test Sphinx-Gallery | ||
""" | ||
|
||
from __future__ import division, absolute_import, print_function, unicode_literals | ||
from __future__ import (division, absolute_import, print_function, | ||
unicode_literals) | ||
import codecs | ||
import os | ||
import tempfile | ||
import re | ||
import shutil | ||
import tempfile | ||
import pytest | ||
from sphinx.application import Sphinx | ||
from sphinx.errors import ExtensionError | ||
from sphinx_gallery.gen_rst import MixedEncodingStringIO | ||
from sphinx_gallery.gen_gallery import DEFAULT_GALLERY_CONF | ||
from sphinx_gallery import sphinx_compatibility | ||
|
||
|
||
|
@@ -162,3 +164,69 @@ def test_config_backreferences(config_app): | |
'gen_modules', 'backreferences') | ||
build_warn = config_app._warning.getvalue() | ||
assert build_warn == "" | ||
|
||
|
||
def _check_order(config_app, key): | ||
index_fname = os.path.join(config_app.outdir, '..', 'ex', 'index.rst') | ||
order = list() | ||
regex = '.*:%s=(.):.*' % key | ||
with codecs.open(index_fname, 'r', 'utf-8') as fid: | ||
for line in fid: | ||
if 'sphx-glr-thumbcontainer' in line: | ||
order.append(int(re.match(regex, line).group(1))) | ||
assert len(order) == 3 | ||
assert order == [1, 2, 3] | ||
|
||
|
||
@pytest.mark.conf_file(content=""" | ||
import sphinx_gallery | ||
extensions = ['sphinx_gallery.gen_gallery'] | ||
sphinx_gallery_conf = { | ||
'examples_dirs': 'src', | ||
'gallery_dirs': 'ex', | ||
}""") | ||
def test_example_sorting_default(config_app): | ||
"""Test sorting of examples by default key (number of code lines).""" | ||
_check_order(config_app, 'lines') | ||
|
||
|
||
@pytest.mark.conf_file(content=""" | ||
import sphinx_gallery | ||
from sphinx_gallery.sorting import FileSizeSortKey | ||
extensions = ['sphinx_gallery.gen_gallery'] | ||
sphinx_gallery_conf = { | ||
'examples_dirs': 'src', | ||
'gallery_dirs': 'ex', | ||
'within_subsection_order': FileSizeSortKey, | ||
}""") | ||
def test_example_sorting_filesize(config_app): | ||
"""Test sorting of examples by filesize.""" | ||
_check_order(config_app, 'filesize') | ||
|
||
|
||
@pytest.mark.conf_file(content=""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests feel like magic to me There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The conf stuff? Agreed but I copy pasted (then mostly simplified) from earlier code so I assumed thus was best practice for the repo |
||
import sphinx_gallery | ||
from sphinx_gallery.sorting import FileNameSortKey | ||
extensions = ['sphinx_gallery.gen_gallery'] | ||
sphinx_gallery_conf = { | ||
'examples_dirs': 'src', | ||
'gallery_dirs': 'ex', | ||
'within_subsection_order': FileNameSortKey, | ||
}""") | ||
def test_example_sorting_filename(config_app): | ||
"""Test sorting of examples by filename.""" | ||
_check_order(config_app, 'filename') | ||
|
||
|
||
@pytest.mark.conf_file(content=""" | ||
import sphinx_gallery | ||
from sphinx_gallery.sorting import ExampleTitleSortKey | ||
extensions = ['sphinx_gallery.gen_gallery'] | ||
sphinx_gallery_conf = { | ||
'examples_dirs': 'src', | ||
'gallery_dirs': 'ex', | ||
'within_subsection_order': ExampleTitleSortKey, | ||
}""") | ||
def test_example_sorting_title(config_app): | ||
"""Test sorting of examples by title.""" | ||
_check_order(config_app, 'title') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should have a check somewhere that sees if one of the allowed keys are given, and if not (e.g., if somebody use some other wonky object) we'd throw an error that says "this must be one of XXX sortkeys"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But people in principle can use their own