From e08f12f848a73142acb74a254eb62a3d6c87d13f Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Sun, 14 Jul 2024 02:16:26 +0200 Subject: [PATCH] toctree: Use document nesting instead of domain nesting when adding domain objects (#12367) Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- CHANGES.rst | 5 +++ sphinx/environment/collectors/toctree.py | 36 +++++++++++------- .../document_scoping.rst | 23 ++++++++++++ .../test-toctree-domain-objects/index.rst | 1 + .../test_environment_toctree.py | 37 ++++++++++++++++++- 5 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 tests/roots/test-toctree-domain-objects/document_scoping.rst diff --git a/CHANGES.rst b/CHANGES.rst index bd7cf7ecef4..d75bde8eb86 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -149,6 +149,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 ------- diff --git a/sphinx/environment/collectors/toctree.py b/sphinx/environment/collectors/toctree.py index 6ea148c631e..dcf4ee178ef 100644 --- a/sphinx/environment/collectors/toctree.py +++ b/sphinx/environment/collectors/toctree.py @@ -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 @@ -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): @@ -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 @@ -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) diff --git a/tests/roots/test-toctree-domain-objects/document_scoping.rst b/tests/roots/test-toctree-domain-objects/document_scoping.rst new file mode 100644 index 00000000000..49aba9e4b11 --- /dev/null +++ b/tests/roots/test-toctree-domain-objects/document_scoping.rst @@ -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() diff --git a/tests/roots/test-toctree-domain-objects/index.rst b/tests/roots/test-toctree-domain-objects/index.rst index 77ee0100960..5f041725a78 100644 --- a/tests/roots/test-toctree-domain-objects/index.rst +++ b/tests/roots/test-toctree-domain-objects/index.rst @@ -4,3 +4,4 @@ :name: mastertoc domains + document_scoping diff --git a/tests/test_environment/test_environment_toctree.py b/tests/test_environment/test_environment_toctree.py index 175c6abe42b..6979a129e47 100644 --- a/tests/test_environment/test_environment_toctree.py +++ b/tests/test_environment/test_environment_toctree.py @@ -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'} @@ -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):