Skip to content

Commit 03c73c4

Browse files
authored
More speedups to section TOC rendering (#1642)
* small refactor, comments, cleanup * docstring cleanups * mark as unsafe for parallel write * Update src/pydata_sphinx_theme/toctree.py
1 parent fff6837 commit 03c73c4

File tree

3 files changed

+96
-64
lines changed

3 files changed

+96
-64
lines changed

src/pydata_sphinx_theme/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -287,4 +287,4 @@ def setup(app: Sphinx) -> Dict[str, str]:
287287
# Include component templates
288288
app.config.templates_path.append(str(theme_path / "components"))
289289

290-
return {"parallel_read_safe": True, "parallel_write_safe": True}
290+
return {"parallel_read_safe": True, "parallel_write_safe": False}

src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@
7979
<div class="bd-container">
8080
<div class="bd-container__inner bd-page-width">
8181
{# Primary sidebar #}
82-
{# If we have no sidebar TOC, pop the TOC component from the sidebar list #}
83-
{% if missing_sidebar_toctree(includehidden=theme_sidebar_includehidden) %}
82+
{# If we have no sidebar TOC, pop the TOC component from the sidebars list #}
83+
{% if suppress_sidebar_toctree(includehidden=theme_sidebar_includehidden | tobool) %}
8484
{% set sidebars = sidebars | reject("in", "sidebar-nav-bs.html") | list %}
8585
{% endif %}
8686
<div class="bd-sidebar-primary bd-sidebar{% if not sidebars %} hide-on-wide{% endif %}">

src/pydata_sphinx_theme/toctree.py

+93-61
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ def add_inline_math(node: Node) -> str:
3131
)
3232

3333

34-
def _get_ancestor_section(app: Sphinx, pagename: str, startdepth: int) -> str:
35-
"""Get the TocTree node `startdepth` levels below the root that dominates `pagename`."""
34+
def _get_ancestor_pagename(app: Sphinx, pagename: str, startdepth: int) -> str:
35+
"""Get the name of `pagename`'s ancestor that is rooted `startdepth` levels below the global root."""
3636
toctree = TocTree(app.env)
3737
if sphinx.version_info[:2] >= (7, 2):
3838
from sphinx.environment.adapters.toctree import _get_toctree_ancestors
@@ -41,49 +41,47 @@ def _get_ancestor_section(app: Sphinx, pagename: str, startdepth: int) -> str:
4141
else:
4242
ancestors = toctree.get_toctree_ancestors(pagename)
4343
try:
44-
return ancestors[-startdepth] # will be a pagename (string)?
44+
out = ancestors[-startdepth]
4545
except IndexError:
4646
# eg for index.rst, but also special pages such as genindex, py-modindex, search
4747
# those pages don't have a "current" element in the toctree, so we can
48-
# directly return an empty string instead of using the default sphinx
48+
# directly return None instead of using the default sphinx
4949
# toctree.get_toctree_for(pagename, app.builder, collapse, **kwargs)
50-
return None
51-
52-
53-
def get_unrendered_local_toctree(app: Sphinx, pagename: str, startdepth: int, **kwargs):
54-
"""Get the "local" (starting at `startdepth`) TocTree containing `pagename`.
55-
56-
This is similar to `context["toctree"](**kwargs)` in sphinx templating,
57-
but using the startdepth-local instead of global TOC tree.
58-
"""
59-
kwargs.setdefault("collapse", True)
60-
if kwargs.get("maxdepth") == "":
61-
kwargs.pop("maxdepth")
62-
toctree = TocTree(app.env)
63-
indexname = _get_ancestor_section(app=app, pagename=pagename, startdepth=startdepth)
64-
if indexname is None:
65-
return None
66-
return get_local_toctree_for_doc(
67-
toctree, indexname, pagename, app.builder, **kwargs
68-
)
50+
out = None
51+
return out, toctree
6952

7053

7154
def add_toctree_functions(
7255
app: Sphinx, pagename: str, templatename: str, context, doctree
7356
) -> None:
7457
"""Add functions so Jinja templates can add toctree objects."""
7558

76-
def missing_sidebar_toctree(startdepth: int = 1, **kwargs):
59+
def suppress_sidebar_toctree(startdepth: int = 1, **kwargs):
7760
"""Check if there's a sidebar TocTree that needs to be rendered.
7861
7962
Parameters:
8063
startdepth : The level of the TocTree at which to start. 0 includes the
8164
entire TocTree for the site; 1 (default) gets the TocTree for the current
8265
top-level section.
8366
84-
kwargs: passed to the Sphinx `toctree` template function.
67+
kwargs : passed to the Sphinx `toctree` template function.
8568
"""
86-
toctree = get_unrendered_local_toctree(app, pagename, startdepth, **kwargs)
69+
ancestorname, toctree_obj = _get_ancestor_pagename(
70+
app=app, pagename=pagename, startdepth=startdepth
71+
)
72+
if ancestorname is None:
73+
return True # suppress
74+
if kwargs.get("includehidden", False):
75+
# if ancestor is found and `includehidden=True` we're guaranteed there's a
76+
# TocTree to be shown, so don't suppress
77+
return False
78+
79+
# we've found an ancestor page, but `includehidden=False` so we can't be sure if
80+
# there's a TocTree fragment that should be shown on this page; unfortunately we
81+
# must resolve the whole TOC subtree to find out
82+
toctree = get_nonroot_toctree(
83+
app, pagename, ancestorname, toctree_obj, **kwargs
84+
)
8785
return toctree is None
8886

8987
@cache
@@ -118,6 +116,9 @@ def generate_header_nav_before_dropdown(n_links_before_dropdown):
118116
if sphinx.version_info[:2] >= (7, 2):
119117
from sphinx.environment.adapters.toctree import _get_toctree_ancestors
120118

119+
# NOTE: `env.toctree_includes` is a dict mapping pagenames to any (possibly
120+
# hidden) TocTree directives on that page (i.e., the "child" pages nested
121+
# under `pagename`).
121122
active_header_page = [
122123
*_get_toctree_ancestors(app.env.toctree_includes, pagename)
123124
]
@@ -127,14 +128,18 @@ def generate_header_nav_before_dropdown(n_links_before_dropdown):
127128
# The final list item will be the top-most ancestor
128129
active_header_page = active_header_page[-1]
129130

130-
# Find the root document because it lists our top-level toctree pages
131-
root = app.env.tocs[app.config.root_doc]
131+
# NOTE: `env.tocs` is a dict mapping pagenames to hierarchical bullet-lists
132+
# ("nodetrees" in Sphinx parlance) of in-page headings (including `toctree::`
133+
# directives). Thus the `tocs` of `root_doc` yields the top-level pages that sit
134+
# just below the root of our site
135+
root_toc = app.env.tocs[app.config.root_doc]
132136

133-
# Iterate through each toctree node in the root document
134-
# Grab the toctree pages and find the relative link + title.
135137
links_html = []
136-
# TODO: use `root.findall(TocTreeNodeClass)` once docutils min version >=0.18.1
137-
for toc in traverse_or_findall(root, TocTreeNodeClass):
138+
# Iterate through each node in the root document toc.
139+
# Grab the toctree pages and find the relative link + title.
140+
for toc in traverse_or_findall(root_toc, TocTreeNodeClass):
141+
# TODO: ↑↑↑ use `root_toc.findall(TocTreeNodeClass)` ↑↑↑
142+
# once docutils min version >=0.18.1
138143
for title, page in toc.attributes["entries"]:
139144
# if the page is using "self" use the correct link
140145
page = toc.attributes["parent"] if page == "self" else page
@@ -262,17 +267,27 @@ def generate_toctree_html(
262267
kind : "sidebar" or "raw". Whether to generate HTML meant for sidebar navigation ("sidebar") or to return the raw BeautifulSoup object ("raw").
263268
startdepth : The level of the toctree at which to start. By default, for the navbar uses the normal toctree (`startdepth=0`), and for the sidebar starts from the second level (`startdepth=1`).
264269
show_nav_level : The level of the navigation bar to toggle as visible on page load. By default, this level is 1, and only top-level pages are shown, with drop-boxes to reveal children. Increasing `show_nav_level` will show child levels as well.
265-
kwargs: passed to the Sphinx `toctree` template function.
270+
kwargs : passed to the Sphinx `toctree` template function.
266271
267272
Returns:
268273
HTML string (if kind == "sidebar") OR BeautifulSoup object (if kind == "raw")
269274
"""
270275
if startdepth == 0:
271276
html_toctree = context["toctree"](**kwargs)
272277
else:
278+
# find relevant ancestor page; some pages (search, genindex) won't have one
279+
ancestorname, toctree_obj = _get_ancestor_pagename(
280+
app=app, pagename=pagename, startdepth=startdepth
281+
)
282+
if ancestorname is None:
283+
raise RuntimeError(
284+
"Template requested to generate a TocTree fragment but no suitable "
285+
"ancestor found to act as root node. Please report this to theme "
286+
"developers."
287+
)
273288
# select the "active" subset of the navigation tree for the sidebar
274-
toctree_element = get_unrendered_local_toctree(
275-
app, pagename, startdepth, **kwargs
289+
toctree_element = get_nonroot_toctree(
290+
app, pagename, ancestorname, toctree_obj, **kwargs
276291
)
277292
html_toctree = app.builder.render_partial(toctree_element)["fragment"]
278293

@@ -394,7 +409,7 @@ def navbar_align_class() -> List[str]:
394409

395410
context["unique_html_id"] = unique_html_id
396411
context["generate_header_nav_html"] = generate_header_nav_html
397-
context["missing_sidebar_toctree"] = missing_sidebar_toctree
412+
context["suppress_sidebar_toctree"] = suppress_sidebar_toctree
398413
context["generate_toctree_html"] = generate_toctree_html
399414
context["generate_toc_html"] = generate_toc_html
400415
context["navbar_align_class"] = navbar_align_class
@@ -459,36 +474,53 @@ def add_collapse_checkboxes(soup: BeautifulSoup) -> None:
459474
element.insert(1, checkbox)
460475

461476

462-
def get_local_toctree_for_doc(
463-
toctree: TocTree, indexname: str, pagename: str, builder, collapse: bool, **kwargs
464-
) -> List[BeautifulSoup]:
465-
"""Get the "local" TocTree containing `pagename` rooted at `indexname`.
466-
467-
The Sphinx equivalent is TocTree.get_toctree_for(), which always uses the "root"
468-
or "global" TocTree:
469-
470-
doctree = self.env.get_doctree(self.env.config.root_doc)
471-
472-
Whereas here we return a subset of the global toctree, rooted at `indexname`
473-
(e.g. starting at a second level for the sidebar).
477+
def get_nonroot_toctree(
478+
app: Sphinx, pagename: str, ancestorname: str, toctree, **kwargs
479+
):
480+
"""Get the partial TocTree (rooted at `ancestorname`) that dominates `pagename`.
481+
482+
Parameters:
483+
app : Sphinx app.
484+
pagename : Name of the current page (as Sphinx knows it; i.e., its relative path
485+
from the documentation root).
486+
ancestorname : Name of a page that dominates `pagename` and that will serve as the
487+
root of the TocTree fragment.
488+
toctree : A Sphinx TocTree object. Since this is always needed when finding the
489+
ancestorname (see _get_ancestor_pagename), it's more efficient to pass it here to
490+
re-use it.
491+
kwargs : passed to the Sphinx `toctree` template function.
492+
493+
This is similar to `context["toctree"](**kwargs)` (AKA `toctree(**kwargs)` within a
494+
Jinja template), or `TocTree.get_toctree_for()`, which always uses the "root"
495+
doctree (i.e., `doctree = self.env.get_doctree(self.env.config.root_doc)`).
474496
"""
475-
partial_doctree = toctree.env.tocs[indexname].deepcopy()
476-
477-
toctrees = []
497+
kwargs.setdefault("collapse", True)
478498
if "maxdepth" not in kwargs or not kwargs["maxdepth"]:
479499
kwargs["maxdepth"] = 0
480500
kwargs["maxdepth"] = int(kwargs["maxdepth"])
481-
kwargs["collapse"] = collapse
482-
483-
# TODO: use `doctree.findall(TocTreeNodeClass)` once docutils min version >=0.18.1
484-
for _node in traverse_or_findall(partial_doctree, TocTreeNodeClass):
485-
# defaults for resolve: prune=True, maxdepth=0, titles_only=False, collapse=False, includehidden=False
486-
_toctree = toctree.resolve(pagename, builder, _node, **kwargs)
487-
if _toctree:
488-
toctrees.append(_toctree)
501+
# starting from ancestor page, recursively parse `toctree::` elements
502+
ancestor_doctree = toctree.env.tocs[ancestorname].deepcopy()
503+
toctrees = []
504+
505+
# for each `toctree::` directive in the ancestor page...
506+
for toctree_node in traverse_or_findall(ancestor_doctree, TocTreeNodeClass):
507+
# TODO: ↑↑↑↑↑↑ use `ancestor_doctree.findall(TocTreeNodeClass)` ↑↑↑↑↑↑
508+
# once docutils min version >=0.18.1
509+
510+
# ... resolve that `toctree::` (recursively get children, prune, collapse, etc)
511+
resolved_toctree = toctree.resolve(
512+
docname=pagename,
513+
builder=app.builder,
514+
toctree=toctree_node,
515+
**kwargs,
516+
)
517+
# ... keep the non-empty ones
518+
if resolved_toctree:
519+
toctrees.append(resolved_toctree)
489520
if not toctrees:
490521
return None
522+
# ... and merge them into a single entity
491523
result = toctrees[0]
492-
for toctree in toctrees[1:]:
493-
result.extend(toctree.children)
524+
for resolved_toctree in toctrees[1:]:
525+
result.extend(resolved_toctree.children)
494526
return result

0 commit comments

Comments
 (0)