Skip to content

Commit e08f12f

Browse files
toctree: Use document nesting instead of domain nesting when adding domain objects (#12367)
Co-authored-by: Adam Turner <[email protected]>
1 parent 159c267 commit e08f12f

File tree

5 files changed

+88
-14
lines changed

5 files changed

+88
-14
lines changed

CHANGES.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ Bugs fixed
149149
Patch by James Addison and Will Lachance.
150150
* #9634: Do not add a fallback language by stripping the country code.
151151
Patch by Alvin Wong.
152+
* #12352: Add domain objects to the table of contents
153+
in the same order as defined in the document.
154+
Previously, each domain used language-specific nesting rules,
155+
which removed control from document authors.
156+
Patch by Jakob Lykke Andersen and Adam Turner.
152157

153158
Testing
154159
-------

sphinx/environment/collectors/toctree.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@ def build_toc(
6868
) -> nodes.bullet_list | None:
6969
# list of table of contents entries
7070
entries: list[Element] = []
71-
# cache of parents -> list item
72-
memo_parents: dict[tuple[str, ...], nodes.list_item] = {}
7371
for sectionnode in node:
7472
# find all toctree nodes in this section and add them
7573
# to the toc (just copying the toctree node which is then
@@ -103,6 +101,8 @@ def build_toc(
103101
entries.append(onlynode)
104102
# check within the section for other node types
105103
elif isinstance(sectionnode, nodes.Element):
104+
# cache of parent node -> list item
105+
memo_parents: dict[nodes.Element, nodes.list_item] = {}
106106
toctreenode: nodes.Node
107107
for toctreenode in sectionnode.findall():
108108
if isinstance(toctreenode, nodes.section):
@@ -114,6 +114,10 @@ def build_toc(
114114
note_toctree(app.env, docname, toctreenode)
115115
# add object signatures within a section to the ToC
116116
elif isinstance(toctreenode, addnodes.desc):
117+
# The desc has one or more nested desc_signature,
118+
# and then a desc_content, which again may have desc nodes.
119+
# Thus, desc is the one we can bubble up to through parents.
120+
entry: nodes.list_item | None = None
117121
for sig_node in toctreenode:
118122
if not isinstance(sig_node, addnodes.desc_signature):
119123
continue
@@ -136,22 +140,28 @@ def build_toc(
136140
para = addnodes.compact_paragraph('', '', reference,
137141
skip_section_number=True)
138142
entry = nodes.list_item('', para)
139-
*parents, _ = sig_node['_toc_parts']
140-
parents = tuple(parents)
141143

142-
# Cache parents tuple
143-
memo_parents[sig_node['_toc_parts']] = entry
144-
145-
# Nest children within parents
146-
if parents and parents in memo_parents:
147-
root_entry = memo_parents[parents]
144+
# Find parent node
145+
parent = sig_node.parent
146+
while parent not in memo_parents and parent != sectionnode:
147+
parent = parent.parent
148+
# Note, it may both be the limit and in memo_parents,
149+
# prefer memo_parents, so we get the nesting.
150+
if parent in memo_parents:
151+
root_entry = memo_parents[parent]
148152
if isinstance(root_entry[-1], nodes.bullet_list):
149153
root_entry[-1].append(entry)
150154
else:
151155
root_entry.append(nodes.bullet_list('', entry))
152-
continue
153-
154-
entries.append(entry)
156+
else:
157+
assert parent == sectionnode
158+
entries.append(entry)
159+
160+
# Save the latest desc_signature as the one we put sub entries in.
161+
# If there are multiple signatures, then the latest is used.
162+
if entry is not None:
163+
# are there any desc nodes without desc_signature nodes?
164+
memo_parents[toctreenode] = entry
155165

156166
if entries:
157167
return nodes.bullet_list('', *entries)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Level 1
2+
=======
3+
4+
.. py:class:: ClassLevel1a
5+
ClassLevel1b
6+
7+
.. py:method:: f()
8+
9+
.. py:method:: ClassLevel1a.g()
10+
11+
.. py:method:: ClassLevel1b.g()
12+
13+
Level 2
14+
-------
15+
16+
.. py:class:: ClassLevel2a
17+
ClassLevel2b
18+
19+
.. py:method:: f()
20+
21+
.. py:method:: ClassLevel2a.g()
22+
23+
.. py:method:: ClassLevel2b.g()

tests/roots/test-toctree-domain-objects/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
:name: mastertoc
55

66
domains
7+
document_scoping

tests/test_environment/test_environment_toctree.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def test_domain_objects(app):
132132

133133
assert app.env.toc_num_entries['index'] == 0
134134
assert app.env.toc_num_entries['domains'] == 9
135-
assert app.env.toctree_includes['index'] == ['domains']
135+
assert app.env.toctree_includes['index'] == ['domains', 'document_scoping']
136136
assert 'index' in app.env.files_to_rebuild['domains']
137137
assert app.env.glob_toctrees == set()
138138
assert app.env.numbered_toctrees == {'index'}
@@ -161,6 +161,41 @@ def test_domain_objects(app):
161161
[list_item, ([compact_paragraph, reference, literal, "HelloWorldPrinter.print()"])])
162162

163163

164+
@pytest.mark.sphinx('dummy', testroot='toctree-domain-objects')
165+
def test_domain_objects_document_scoping(app):
166+
app.build()
167+
168+
# tocs
169+
toctree = app.env.tocs['document_scoping']
170+
assert_node(
171+
toctree,
172+
[bullet_list, list_item, (
173+
compact_paragraph, # [0][0]
174+
[bullet_list, ( # [0][1]
175+
[list_item, compact_paragraph, reference, literal, 'ClassLevel1a'], # [0][1][0]
176+
[list_item, ( # [0][1][1]
177+
[compact_paragraph, reference, literal, 'ClassLevel1b'], # [0][1][1][0]
178+
[bullet_list, list_item, compact_paragraph, reference, literal, 'ClassLevel1b.f()'], # [0][1][1][1][0]
179+
)],
180+
[list_item, compact_paragraph, reference, literal, 'ClassLevel1a.g()'], # [0][1][2]
181+
[list_item, compact_paragraph, reference, literal, 'ClassLevel1b.g()'], # [0][1][3]
182+
[list_item, ( # [0][1][4]
183+
[compact_paragraph, reference, 'Level 2'], # [0][1][4][0]
184+
[bullet_list, ( # [0][1][4][1]
185+
[list_item, compact_paragraph, reference, literal, 'ClassLevel2a'], # [0][1][4][1][0]
186+
[list_item, ( # [0][1][4][1][1]
187+
[compact_paragraph, reference, literal, 'ClassLevel2b'], # [0][1][4][1][1][0]
188+
[bullet_list, list_item, compact_paragraph, reference, literal, 'ClassLevel2b.f()'], # [0][1][4][1][1][1][0]
189+
)],
190+
[list_item, compact_paragraph, reference, literal, 'ClassLevel2a.g()'], # [0][1][4][1][2]
191+
[list_item, compact_paragraph, reference, literal, 'ClassLevel2b.g()'], # [0][1][4][1][3]
192+
)],
193+
)],
194+
)],
195+
)],
196+
)
197+
198+
164199
@pytest.mark.sphinx('xml', testroot='toctree')
165200
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
166201
def test_document_toc(app):

0 commit comments

Comments
 (0)