Skip to content

Commit

Permalink
ENH: Allow plain list as subsection_order and support a wildcard (#1295)
Browse files Browse the repository at this point in the history
  • Loading branch information
timhoffm committed Apr 26, 2024
1 parent f0e716e commit 2120054
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 35 deletions.
46 changes: 32 additions & 14 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -452,33 +452,51 @@ Sorting gallery subsections

Gallery subsections are sorted by default alphabetically by their folder
name, and as such you can always organize them by changing your folder
names. An alternative option is to use a sortkey to organize those
subsections. We provide an explicit order sortkey where you have to define
the order of all subfolders in your galleries::
names. Alternatively, you can specify the order via the config value
'subsection_order' by providing a list of the subsections as paths
relative to :file:`conf.py` in the desired order::

from sphinx_gallery.sorting import ExplicitOrder
sphinx_gallery_conf = {
...
'examples_dirs': ['../examples','../tutorials'],
'subsection_order': ExplicitOrder(['../examples/sin_func',
'../examples/no_output',
'../tutorials/seaborn']),
'subsection_order': ['../examples/sin_func',
'../examples/no_output',
'../tutorials/seaborn'],
}

Here we build 2 main galleries `examples` and `tutorials`, each of them
with subsections. To specify their order explicitly in the gallery we
import :class:`sphinx_gallery.sorting.ExplicitOrder` and initialize it with
the list of all subfolders with their paths relative to `conf.py` in the
order you prefer them to appear. Keep in mind that we use a single sort key
with subsections. You must list all subsections. If that's too cumbersome,
one entry can be "*", which will collect all not-listed subsections, e.g.
``["first_subsection", "*", "last_subsection"]``.

Even more generally, you can set 'subsection_order' to any callable, which
will be used as sorting key function on the subsection paths. See
:ref:`own_sort_keys` for more information.

In fact, the
above list is a convenience shortcut and it is internally wrapped in
:class:`sphinx_gallery.sorting.ExplicitOrder` as a sortkey.

.. note::

Sphinx-Gallery <0.16.0 required to wrap the list in
:class:`.ExplicitOrder` ::

from sphinx_gallery.sorting import ExplicitOrder
sphinx_gallery_conf = {
...
'subsection_order': ExplicitOrder([...])
}

This pattern is discouraged in favor of passing the simple list.

Keep in mind that we use a single sort key
for all the galleries that are built, thus we include the prefix of each
gallery in the corresponding subsection folders. One does not define a
sortkey per gallery. You can use Linux paths, and if your documentation is
built in a Windows system, paths will be transformed to work accordingly,
the converse does not hold.

If you implement your own sort key, it will be passed the subfolder path,
relative to the ``conf.py`` file. See :ref:`own_sort_keys` for more information.

.. _within_gallery_order:

Sorting gallery examples
Expand Down
4 changes: 4 additions & 0 deletions sphinx_gallery/gen_gallery.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
from .interactive_example import create_jupyterlite_contents
from .directives import MiniGallery, ImageSg, imagesg_addnode
from .recommender import ExampleRecommender, _write_recommendations
from .sorting import ExplicitOrder


_KNOWN_CSS = (
"sg_gallery",
Expand Down Expand Up @@ -448,6 +450,8 @@ def get_subsections(srcdir, examples_dir, gallery_conf, check_for_index=True):
sortkey = None
else:
(sortkey,) = _get_callables(gallery_conf, "subsection_order")
if isinstance(sortkey, list):
sortkey = ExplicitOrder(sortkey)
subfolders = [subfolder for subfolder in os.listdir(examples_dir)]
if check_for_index:
subfolders = [
Expand Down
50 changes: 40 additions & 10 deletions sphinx_gallery/sorting.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
class ExplicitOrder:
"""Sorting key for all gallery subsections.
This requires all folders to be listed otherwise an exception is raised.
All subsections folders must be listed, otherwise an exception is raised.
However, you can add '*' as a placeholder to the list. All not-listed
subsection folders will be inserted at the given position and no
exception is raised.
Parameters
----------
Expand All @@ -40,18 +43,45 @@ def __init__(self, ordered_list):
"the paths of each gallery subfolder"
)

self.ordered_list = list(os.path.normpath(path) for path in ordered_list)
self.ordered_list = [
"*" if path == "*" else os.path.normpath(path) for path in ordered_list
]
try:
i = ordered_list.index("*")
self.has_wildcard = True
self.ordered_list_start = self.ordered_list[:i]
self.ordered_list_end = self.ordered_list[i + 1 :]
except ValueError: # from index("*")
self.has_wildcard = False
self.ordered_list_start = []
self.ordered_list_end = self.ordered_list

def __call__(self, item):
"""Return index of item in `ordered_list`, raising error if it is missing."""
if item in self.ordered_list:
return self.ordered_list.index(item)
"""
Return an integer suited for ordering the items.
If the ordered_list contains a wildcard "*", items before "*" will return
negative numbers, items after "*" will have positive numbers, and
not-listed items will return 0.
If there is no wildcard, all items with return positive numbers, and
not-listed items will raise a ConfigError.
"""
if item in self.ordered_list_start:
return self.ordered_list_start.index(item) - len(self.ordered_list_start)
elif item in self.ordered_list_end:
return self.ordered_list_end.index(item) + 1
else:
raise ConfigError(
"If you use an explicit folder ordering, you "
"must specify all folders. Explicit order not "
"found for {}".format(item)
)
if self.has_wildcard:
return 0
else:
raise ConfigError(
"The subsection folder {!r} was not found in the "
"'subsection_order' config. If you use an explicit "
"'subsection_order', you must specify all subsection folders "
"or add '*' as a wildcard to collect all not-listed subsection "
"folders.".format(item)
)

def __repr__(self):
return f"<{self.__class__.__name__} : {self.ordered_list}>"
Expand Down
35 changes: 24 additions & 11 deletions sphinx_gallery/tests/test_sorting.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,11 @@

def test_ExplicitOrder_sorting_key():
"""Test ExplicitOrder."""
all_folders = ["e", "f", "d", "c", "01b", "a"]
explicit_folders = ["f", "d"]
key = ExplicitOrder(explicit_folders)
sorted_folders = sorted(["d", "f"], key=key)
assert sorted_folders == explicit_folders

# Test fails on wrong input
with pytest.raises(ConfigError) as excinfo:
ExplicitOrder("nope")
excinfo.match("ExplicitOrder sorting key takes a list")

# Test missing folder
with pytest.raises(ConfigError) as excinfo:
sorted_folders = sorted(all_folders, key=key)
excinfo.match("If you use an explicit folder ordering")

# str(obj) stability for sphinx non-rebuilds
assert str(key).startswith("<ExplicitOrder : ")
assert str(key) == str(ExplicitOrder(explicit_folders))
Expand All @@ -51,6 +40,30 @@ def test_ExplicitOrder_sorting_key():
assert isinstance(out, type_), type(out)


def test_ExplicitOrder_sorting_wildcard():
# wildcard at start
sorted_folders = sorted(list("abcd"), key=ExplicitOrder(["*", "b", "a"]))
assert sorted_folders == ["c", "d", "b", "a"]

# wildcard in the middle
sorted_folders = sorted(list("abcde"), key=ExplicitOrder(["b", "a", "*", "c"]))
assert sorted_folders == ["b", "a", "d", "e", "c"]

# wildcard at end
sorted_folders = sorted(list("abcd"), key=ExplicitOrder(["b", "a", "*"]))
assert sorted_folders == ["b", "a", "c", "d"]


def test_ExplicitOrder_sorting_errors():
# Test fails on wrong input
with pytest.raises(ConfigError, match="ExplicitOrder sorting key takes a list"):
ExplicitOrder("nope")

# Test folder not listed in ExplicitOrder
with pytest.raises(ConfigError, match="subsection folder .* was not found"):
sorted(["a", "b", "c"], key=ExplicitOrder(["a", "b"]))


def test_Function_sorting_key():
data = [(1, 0), (3, 2), (5, 4), (7, 6), (9, 8)]

Expand Down

0 comments on commit 2120054

Please sign in to comment.