Skip to content

Commit fcde006

Browse files
authored
Fix: more efficient determination of when to hide primary sidebar (#1609)
* minor refactors suggested by Ruff * move TOC generation from layout.html to sidebar-primary.html * replace @lru_cache(None) with plain @cache * get sidebar TOC length without rendering it * fixup rebase/stash snafu * bugfix
1 parent 068ac7b commit fcde006

File tree

3 files changed

+67
-53
lines changed

3 files changed

+67
-53
lines changed

src/pydata_sphinx_theme/theme/pydata_sphinx_theme/components/sidebar-nav-bs.html

+11-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,15 @@
22
<nav class="bd-docs-nav bd-links"
33
aria-label="{{ _('Section Navigation') }}">
44
<p class="bd-links__title" role="heading" aria-level="1">{{ _("Section Navigation") }}</p>
5-
<div class="bd-toc-item navbar-nav">{{ sidebar_nav_html }}</div>
5+
<div class="bd-toc-item navbar-nav">
6+
{{- generate_toctree_html(
7+
"sidebar",
8+
show_nav_level=theme_show_nav_level|int,
9+
maxdepth=theme_navigation_depth|int,
10+
collapse=theme_collapse_navigation|tobool,
11+
includehidden=True,
12+
titles_only=True
13+
)
14+
-}}
15+
</div>
616
</nav>

src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html

+5-16
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,8 @@
66
{% endset %}
77
{%- extends "basic/layout.html" %}
88
{%- import "static/webpack-macros.html" as _webpack with context %}
9-
{# Metadata and asset linking #}
10-
{# Create the sidebar links HTML here to re-use in a few places #}
11-
{# If we have no sidebar links, pop the links component from the sidebar list #}
12-
{%- set sidebar_nav_html = generate_toctree_html("sidebar",
13-
show_nav_level=theme_show_nav_level|int,
14-
maxdepth=theme_navigation_depth|int,
15-
collapse=theme_collapse_navigation|tobool,
16-
includehidden=True,
17-
titles_only=True)
18-
-%}
19-
{% if sidebar_nav_html | length == 0 %}
20-
{% set sidebars = sidebars | reject("in", "sidebar-nav-bs.html") | list %}
21-
{% endif %}
229
{# A flag for whether we include a secondary sidebar based on the page metadata #}
23-
{% set remove_sidebar_secondary = (meta is defined and meta is not none
24-
and 'html_theme.sidebar_secondary.remove' in meta)
25-
or not theme_secondary_sidebar_items %}
10+
{% set remove_sidebar_secondary = (meta is defined and meta is not none and 'html_theme.sidebar_secondary.remove' in meta) or not theme_secondary_sidebar_items %}
2611
{%- block css %}
2712
{# The data-cfasync attribute disables CloudFlare's Rocket loader so that #}
2813
{# mode/theme are correctly set before the browser renders the page. #}
@@ -96,6 +81,10 @@
9681
<div class="bd-container">
9782
<div class="bd-container__inner bd-page-width">
9883
{# Primary sidebar #}
84+
{# If we have no sidebar TOC, pop the TOC component from the sidebar list #}
85+
{% if get_sidebar_toctree_length(show_nav_level=theme_show_nav_level|int) == 0 %}
86+
{% set sidebars = sidebars | reject("in", "sidebar-nav-bs.html") | list %}
87+
{% endif %}
9988
<div class="bd-sidebar-primary bd-sidebar{% if not sidebars %} hide-on-wide{% endif %}">
10089
{% include "sections/sidebar-primary.html" %}
10190
</div>

src/pydata_sphinx_theme/toctree.py

+51-36
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Methods to build the toctree used in the html pages."""
22

3-
from functools import lru_cache
3+
from functools import cache
44
from itertools import count
55
from typing import Iterator, List, Union
66
from urllib.parse import urlparse
@@ -32,12 +32,48 @@ def add_inline_math(node: Node) -> str:
3232
)
3333

3434

35+
def get_unrendered_local_toctree(
36+
app: Sphinx, pagename: str, startdepth: int, collapse: bool = True, **kwargs
37+
):
38+
"""."""
39+
if "includehidden" not in kwargs:
40+
kwargs["includehidden"] = False
41+
if kwargs.get("maxdepth") == "":
42+
kwargs.pop("maxdepth")
43+
44+
toctree = TocTree(app.env)
45+
if sphinx.version_info[:2] >= (7, 2):
46+
from sphinx.environment.adapters.toctree import _get_toctree_ancestors
47+
48+
ancestors = [*_get_toctree_ancestors(app.env.toctree_includes, pagename)]
49+
else:
50+
ancestors = toctree.get_toctree_ancestors(pagename)
51+
try:
52+
indexname = ancestors[-startdepth]
53+
except IndexError:
54+
# eg for index.rst, but also special pages such as genindex, py-modindex, search
55+
# those pages don't have a "current" element in the toctree, so we can
56+
# directly return an empty string instead of using the default sphinx
57+
# toctree.get_toctree_for(pagename, app.builder, collapse, **kwargs)
58+
return ""
59+
60+
return get_local_toctree_for(
61+
toctree, indexname, pagename, app.builder, collapse, **kwargs
62+
)
63+
64+
3565
def add_toctree_functions(
3666
app: Sphinx, pagename: str, templatename: str, context, doctree
3767
) -> None:
3868
"""Add functions so Jinja templates can add toctree objects."""
3969

40-
@lru_cache(maxsize=None)
70+
def get_sidebar_toctree_length(
71+
startdepth: int = 1, show_nav_level: int = 1, **kwargs
72+
):
73+
toctree = get_unrendered_local_toctree(app, pagename, startdepth)
74+
return 0 if toctree is None else len(toctree)
75+
76+
@cache
4177
def get_or_create_id_generator(base_id: str) -> Iterator[str]:
4278
for n in count(start=1):
4379
if n == 1:
@@ -53,7 +89,7 @@ def unique_html_id(base_id: str):
5389
"""
5490
return next(get_or_create_id_generator(base_id))
5591

56-
@lru_cache(maxsize=None)
92+
@cache
5793
def generate_header_nav_before_dropdown(n_links_before_dropdown):
5894
"""The cacheable part."""
5995
try:
@@ -191,7 +227,7 @@ def generate_header_nav_html(
191227

192228
# Cache this function because it is expensive to run, and because Sphinx
193229
# somehow runs this twice in some circumstances in unpredictable ways.
194-
@lru_cache(maxsize=None)
230+
@cache
195231
def generate_toctree_html(
196232
kind: str, startdepth: int = 1, show_nav_level: int = 1, **kwargs
197233
) -> Union[BeautifulSoup, str]:
@@ -241,7 +277,7 @@ def generate_toctree_html(
241277
if kind == "sidebar":
242278
# Add bootstrap classes for first `ul` items
243279
for ul in soup("ul", recursive=False):
244-
ul.attrs["class"] = ul.attrs.get("class", []) + ["nav", "bd-sidenav"]
280+
ul.attrs["class"] = [*ul.attrs.get("class", []), "nav", "bd-sidenav"]
245281

246282
# Add collapse boxes for parts/captions.
247283
# Wraps the TOC part in an extra <ul> to behave like chapters with toggles
@@ -276,7 +312,7 @@ def generate_toctree_html(
276312

277313
return soup
278314

279-
@lru_cache(maxsize=None)
315+
@cache
280316
def generate_toc_html(kind: str = "html") -> BeautifulSoup:
281317
"""Return the within-page TOC links in HTML."""
282318
if "toc" not in context:
@@ -289,22 +325,22 @@ def add_header_level_recursive(ul, level):
289325
if ul is None:
290326
return
291327
if level <= (context["theme_show_toc_level"] + 1):
292-
ul["class"] = ul.get("class", []) + ["visible"]
328+
ul["class"] = [*ul.get("class", []), "visible"]
293329
for li in ul("li", recursive=False):
294-
li["class"] = li.get("class", []) + [f"toc-h{level}"]
330+
li["class"] = [*li.get("class", []), f"toc-h{level}"]
295331
add_header_level_recursive(li.find("ul", recursive=False), level + 1)
296332

297333
add_header_level_recursive(soup.find("ul"), 1)
298334

299335
# Add in CSS classes for bootstrap
300336
for ul in soup("ul"):
301-
ul["class"] = ul.get("class", []) + ["nav", "section-nav", "flex-column"]
337+
ul["class"] = [*ul.get("class", []), "nav", "section-nav", "flex-column"]
302338

303339
for li in soup("li"):
304-
li["class"] = li.get("class", []) + ["nav-item", "toc-entry"]
340+
li["class"] = [*li.get("class", []), "nav-item", "toc-entry"]
305341
if li.find("a"):
306342
a = li.find("a")
307-
a["class"] = a.get("class", []) + ["nav-link"]
343+
a["class"] = [*a.get("class", []), "nav-link"]
308344

309345
# If we only have one h1 header, assume it's a title
310346
h1_headers = soup.select(".toc-h1")
@@ -342,6 +378,7 @@ def navbar_align_class() -> List[str]:
342378

343379
context["unique_html_id"] = unique_html_id
344380
context["generate_header_nav_html"] = generate_header_nav_html
381+
context["get_sidebar_toctree_length"] = get_sidebar_toctree_length
345382
context["generate_toctree_html"] = generate_toctree_html
346383
context["generate_toc_html"] = generate_toc_html
347384
context["navbar_align_class"] = navbar_align_class
@@ -368,7 +405,7 @@ def add_collapse_checkboxes(soup: BeautifulSoup) -> None:
368405
continue
369406

370407
# Add a class to indicate that this has children.
371-
element["class"] = classes + ["has-children"]
408+
element["class"] = [*classes, "has-children"]
372409

373410
# We're gonna add a checkbox.
374411
toctree_checkbox_count += 1
@@ -455,29 +492,7 @@ def index_toctree(
455492
# returning:
456493
# return self.render_partial(TocTree(self.env).get_toctree_for(
457494
# pagename, self, collapse, **kwargs))['fragment']
458-
459-
if "includehidden" not in kwargs:
460-
kwargs["includehidden"] = False
461-
if kwargs.get("maxdepth") == "":
462-
kwargs.pop("maxdepth")
463-
464-
toctree = TocTree(app.env)
465-
if sphinx.version_info[:2] >= (7, 2):
466-
from sphinx.environment.adapters.toctree import _get_toctree_ancestors
467-
468-
ancestors = [*_get_toctree_ancestors(app.env.toctree_includes, pagename)]
469-
else:
470-
ancestors = toctree.get_toctree_ancestors(pagename)
471-
try:
472-
indexname = ancestors[-startdepth]
473-
except IndexError:
474-
# eg for index.rst, but also special pages such as genindex, py-modindex, search
475-
# those pages don't have a "current" element in the toctree, so we can
476-
# directly return an empty string instead of using the default sphinx
477-
# toctree.get_toctree_for(pagename, app.builder, collapse, **kwargs)
478-
return ""
479-
480-
toctree_element = get_local_toctree_for(
481-
toctree, indexname, pagename, app.builder, collapse, **kwargs
495+
toctree_element = get_unrendered_local_toctree(
496+
app, pagename, startdepth, collapse, **kwargs
482497
)
483498
return app.builder.render_partial(toctree_element)["fragment"]

0 commit comments

Comments
 (0)