Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

toctree, use document nesting instead of domain nesting when adding domain objects #12367

Merged
merged 6 commits into from
Jul 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ Bugs fixed
Patch by James Addison and Will Lachance.
* #9634: Do not add a fallback language by stripping the country code.
Patch by Alvin Wong.
* #12352: Add domain objects to the table of contents
in the same order as defined in the document.
Previously, each domain used language-specific nesting rules,
which removed control from document authors.
Patch by Jakob Lykke Andersen and Adam Turner.

Testing
-------
Expand Down
36 changes: 23 additions & 13 deletions sphinx/environment/collectors/toctree.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,6 @@ def build_toc(
) -> nodes.bullet_list | None:
# list of table of contents entries
entries: list[Element] = []
# cache of parents -> list item
memo_parents: dict[tuple[str, ...], nodes.list_item] = {}
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
Expand Down Expand Up @@ -103,6 +101,8 @@ def build_toc(
entries.append(onlynode)
# check within the section for other node types
elif isinstance(sectionnode, nodes.Element):
# cache of parent node -> list item
memo_parents: dict[nodes.Element, nodes.list_item] = {}
toctreenode: nodes.Node
for toctreenode in sectionnode.findall():
if isinstance(toctreenode, nodes.section):
Expand All @@ -114,6 +114,10 @@ def build_toc(
note_toctree(app.env, docname, toctreenode)
# add object signatures within a section to the ToC
elif isinstance(toctreenode, addnodes.desc):
# The desc has one or more nested desc_signature,
# and then a desc_content, which again may have desc nodes.
# Thus, desc is the one we can bubble up to through parents.
entry: nodes.list_item | None = None
for sig_node in toctreenode:
if not isinstance(sig_node, addnodes.desc_signature):
continue
Expand All @@ -136,22 +140,28 @@ def build_toc(
para = addnodes.compact_paragraph('', '', reference,
skip_section_number=True)
entry = nodes.list_item('', para)
*parents, _ = sig_node['_toc_parts']
parents = tuple(parents)

# Cache parents tuple
memo_parents[sig_node['_toc_parts']] = entry

# Nest children within parents
if parents and parents in memo_parents:
root_entry = memo_parents[parents]
# Find parent node
parent = sig_node.parent
while parent not in memo_parents and parent != sectionnode:
parent = parent.parent
# Note, it may both be the limit and in memo_parents,
# prefer memo_parents, so we get the nesting.
if parent in memo_parents:
root_entry = memo_parents[parent]
if isinstance(root_entry[-1], nodes.bullet_list):
root_entry[-1].append(entry)
else:
root_entry.append(nodes.bullet_list('', entry))
continue

entries.append(entry)
else:
assert parent == sectionnode
entries.append(entry)

# Save the latest desc_signature as the one we put sub entries in.
# If there are multiple signatures, then the latest is used.
if entry is not None:
# are there any desc nodes without desc_signature nodes?
memo_parents[toctreenode] = entry

if entries:
return nodes.bullet_list('', *entries)
Expand Down
23 changes: 23 additions & 0 deletions tests/roots/test-toctree-domain-objects/document_scoping.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Level 1
=======

.. py:class:: ClassLevel1a
ClassLevel1b

.. py:method:: f()

.. py:method:: ClassLevel1a.g()

.. py:method:: ClassLevel1b.g()

Level 2
-------

.. py:class:: ClassLevel2a
ClassLevel2b

.. py:method:: f()

.. py:method:: ClassLevel2a.g()

.. py:method:: ClassLevel2b.g()
1 change: 1 addition & 0 deletions tests/roots/test-toctree-domain-objects/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
:name: mastertoc

domains
document_scoping
37 changes: 36 additions & 1 deletion tests/test_environment/test_environment_toctree.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def test_domain_objects(app):

assert app.env.toc_num_entries['index'] == 0
assert app.env.toc_num_entries['domains'] == 9
assert app.env.toctree_includes['index'] == ['domains']
assert app.env.toctree_includes['index'] == ['domains', 'document_scoping']
assert 'index' in app.env.files_to_rebuild['domains']
assert app.env.glob_toctrees == set()
assert app.env.numbered_toctrees == {'index'}
Expand Down Expand Up @@ -161,6 +161,41 @@ def test_domain_objects(app):
[list_item, ([compact_paragraph, reference, literal, "HelloWorldPrinter.print()"])])


@pytest.mark.sphinx('dummy', testroot='toctree-domain-objects')
def test_domain_objects_document_scoping(app):
app.build()

# tocs
toctree = app.env.tocs['document_scoping']
assert_node(
toctree,
[bullet_list, list_item, (
compact_paragraph, # [0][0]
[bullet_list, ( # [0][1]
[list_item, compact_paragraph, reference, literal, 'ClassLevel1a'], # [0][1][0]
[list_item, ( # [0][1][1]
[compact_paragraph, reference, literal, 'ClassLevel1b'], # [0][1][1][0]
[bullet_list, list_item, compact_paragraph, reference, literal, 'ClassLevel1b.f()'], # [0][1][1][1][0]
)],
[list_item, compact_paragraph, reference, literal, 'ClassLevel1a.g()'], # [0][1][2]
[list_item, compact_paragraph, reference, literal, 'ClassLevel1b.g()'], # [0][1][3]
[list_item, ( # [0][1][4]
[compact_paragraph, reference, 'Level 2'], # [0][1][4][0]
[bullet_list, ( # [0][1][4][1]
[list_item, compact_paragraph, reference, literal, 'ClassLevel2a'], # [0][1][4][1][0]
[list_item, ( # [0][1][4][1][1]
[compact_paragraph, reference, literal, 'ClassLevel2b'], # [0][1][4][1][1][0]
[bullet_list, list_item, compact_paragraph, reference, literal, 'ClassLevel2b.f()'], # [0][1][4][1][1][1][0]
)],
[list_item, compact_paragraph, reference, literal, 'ClassLevel2a.g()'], # [0][1][4][1][2]
[list_item, compact_paragraph, reference, literal, 'ClassLevel2b.g()'], # [0][1][4][1][3]
)],
)],
)],
)],
)


@pytest.mark.sphinx('xml', testroot='toctree')
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
def test_document_toc(app):
Expand Down