Skip to content

Instantly share code, notes, and snippets.

@asmeurer
Forked from agoose77/sphinx-toctree-signature.py
Last active August 29, 2022 20:00
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save asmeurer/5009f8845f864bd671769d10e07d1184 to your computer and use it in GitHub Desktop.
Save asmeurer/5009f8845f864bd671769d10e07d1184 to your computer and use it in GitHub Desktop.
from typing import List, TypeVar
from docutils import nodes
from docutils.nodes import Element
from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.environment.adapters.toctree import TocTree
import sphinx.environment.collectors.toctree as toctree_collector
from sphinx.transforms import SphinxContentsFilter
from sphinx.util import logging
N = TypeVar("N")
logger = logging.getLogger(__name__)
# Most of this is copied from Sphinx
class BetterTocTreeCollector(toctree_collector.TocTreeCollector):
def process_doc(self, app: Sphinx, doctree: nodes.document) -> None:
"""Build a TOC from the doctree and store it in the inventory."""
docname = app.env.docname
numentries = [0] # nonlocal again...
# This is changed to a generator, and the class condition removed
def traverse_in_section(node: Element) -> List[N]:
"""Like traverse(), but stay within the same section."""
yield node
for child in node.children:
if isinstance(child, nodes.section):
continue
elif isinstance(child, nodes.Element):
yield from traverse_in_section(child)
def build_toc(node: Element, depth: int = 1) -> nodes.bullet_list:
# The logic here is a bit confusing.
# It looks like section nodes are expected to be top-level within a section.
entries: List[Element] = []
current_class = None
for sectionnode in node:
# find all toctree nodes in this section and add them
# to the toc (just copying the toctree node which is then
# resolved in self.get_and_resolve_doctree)
if isinstance(sectionnode, nodes.section):
title = sectionnode[0]
# copy the contents of the section title, but without references
# and unnecessary stuff
visitor = SphinxContentsFilter(doctree)
title.walkabout(visitor)
nodetext = visitor.get_entry_text()
# if nodetext and nodetext[0] == "ak.ArrayBuilder":
# print(node)
# break
if not numentries[0]:
# for the very first toc entry, don't add an anchor
# as it is the file's title anyway
anchorname = ""
else:
anchorname = "#" + sectionnode["ids"][0]
numentries[0] += 1
# make these nodes:
# list_item -> compact_paragraph -> reference
reference = nodes.reference(
"",
"",
internal=True,
refuri=docname,
anchorname=anchorname,
*nodetext
)
para = addnodes.compact_paragraph("", "", reference)
item: Element = nodes.list_item("", para)
sub_item = build_toc(sectionnode, depth + 1)
if sub_item:
item += sub_item
entries.append(item)
elif isinstance(sectionnode, addnodes.only):
onlynode = addnodes.only(expr=sectionnode["expr"])
blist = build_toc(sectionnode, depth)
if blist:
onlynode += blist.children
entries.append(onlynode)
# Otherwise, for a generic element we allow recursion into the section
elif isinstance(sectionnode, nodes.Element):
for node in traverse_in_section(sectionnode):
if isinstance(node, addnodes.toctree):
item = node.copy()
entries.append(item)
# important: do the inventory stuff
TocTree(app.env).note(docname, node)
# For signatures within some section, we add them to the ToC
elif isinstance(node, addnodes.desc):
title = node.children[0]
fullname = title.attributes['fullname']
classname = title.attributes['class']
nodetype = node.attributes['objtype']
if classname != current_class:
current_class = fullname
else:
subtoc = build_toc([node], depth + 1)
if subtoc:
entries.append(subtoc)
continue
if nodetype in ['function', 'method']:
fullname += '()'
nodetext = [nodes.literal(fullname, fullname)]
if not numentries[0]:
# for the very first toc entry, don't add an anchor
# as it is the file's title anyway
anchorname = ""
elif not title["ids"]:
# Skip entries with :noindex: (they do not get anchors)
continue
else:
anchorname = "#" + title["ids"][0]
numentries[0] += 1
# make these nodes:
# list_item -> compact_paragraph -> reference
reference = nodes.reference(
"",
"",
internal=True,
refuri=docname,
anchorname=anchorname,
*nodetext
)
para = addnodes.compact_paragraph("", "", reference)
item: Element = nodes.list_item("", para)
entries.append(item)
# Glossary entries
elif isinstance(node, nodes.term):
nodetext = []
for n in node.children:
if isinstance(n, addnodes.pending_xref):
nodetext.extend(n.children)
else:
nodetext.append(n)
if not numentries[0]:
# for the very first toc entry, don't add an anchor
# as it is the file's title anyway
anchorname = ""
elif not node["ids"]:
continue
else:
anchorname = "#" + node["ids"][0]
numentries[0] += 1
# make these nodes:
# list_item -> compact_paragraph -> reference
reference = nodes.reference(
"",
"",
*nodetext,
internal=True,
refuri=docname,
anchorname=anchorname,
)
para = addnodes.compact_paragraph("", "", reference)
item: Element = nodes.list_item("", para)
entries.append(item)
if entries:
return nodes.bullet_list("", *entries)
return None
toc = build_toc(doctree)
assert docname in app.env.tocs
if toc:
app.env.tocs[docname] = toc
else:
app.env.tocs[docname] = nodes.bullet_list("")
app.env.toc_num_entries[docname] = numentries[0]
def setup(app):
app.add_env_collector(BetterTocTreeCollector)
@tony
Copy link

tony commented Aug 28, 2022

@asmeurer Is this fine to re-use in projects (license MIT?)

re: vcs-python/libvcs#400

@asmeurer
Copy link
Author

You can use my changes under the same license as Sphinx (which is what it was already licensed as since that's where some of the code was taken from).

This is starting to get out of hand for gists. I think we need to just make this into a real Sphinx extension. Even if that extension eventually becomes moot by it being upstreamed into Sphinx, it will still be easier to at least manage multiple people making changes if this is in a real repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment