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

Add contents entries for domain objects #10807

Merged
merged 31 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
90c473f
Remove `traverse_in_section()`, use `node.findall()`
AA-Turner Sep 7, 2022
22b639d
Update type signature for `build_toc()`
AA-Turner Sep 7, 2022
9028cae
Factor out `_make_anchor_name()`
AA-Turner Sep 7, 2022
6f64b9b
Add processing for signature description nodes
AA-Turner Sep 7, 2022
70d8da9
Support content in `py:module` and `js:module`
AA-Turner Sep 7, 2022
64d5993
Add CHANGES entry
AA-Turner Sep 7, 2022
241b556
Add tests
AA-Turner Sep 7, 2022
a3aa81b
Remove class name from Python methods
AA-Turner Sep 8, 2022
770df1b
Update test for output format
AA-Turner Sep 8, 2022
610c73f
Remove `literal` styling
AA-Turner Sep 8, 2022
61edbf6
Remove `literal` styling
AA-Turner Sep 8, 2022
1d578d2
Update documentation for modules
AA-Turner Sep 8, 2022
7c29739
Add configuration for ToC qualification control
AA-Turner Sep 8, 2022
ee09e2c
Delegate name formatting to domains
AA-Turner Sep 8, 2022
8d877f1
Fix for objects which do not call `ObjectDescription.run()`
AA-Turner Sep 8, 2022
081eeac
Typo
AA-Turner Sep 8, 2022
27bbd09
Ignore W503
AA-Turner Sep 8, 2022
285ae68
Reinstate `literal` styling
AA-Turner Sep 10, 2022
a4bacc0
Merge branch '5.x' into auto-toc
AA-Turner Sep 10, 2022
6acced9
Update parent rendering control
AA-Turner Sep 10, 2022
a8e6196
Update documentation
AA-Turner Sep 10, 2022
a20ba85
Implement RST domain
AA-Turner Sep 10, 2022
3a4778f
Add the `noindexentry` and `noindex` flags to more domains
AA-Turner Sep 11, 2022
b9b24ff
Process entries per signature node
AA-Turner Sep 11, 2022
0ba7b5c
Indentation
AA-Turner Sep 12, 2022
ac58158
Update test
AA-Turner Sep 12, 2022
c35ce00
Fix `_object_hierarchy_parts` for invalid input
AA-Turner Sep 12, 2022
12bd9ec
Merge branch '5.x' into auto-toc
AA-Turner Sep 12, 2022
ac47b35
Merge branch '5.x' into auto-toc
AA-Turner Sep 12, 2022
08775b6
Merge branch '5.x' into auto-toc
AA-Turner Sep 12, 2022
fc82407
Fix parens dot
AA-Turner Sep 12, 2022
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
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Features added
* #10755: linkcheck: Check the source URL of raw directives that use the ``url``
option.
* #10781: Allow :rst:role:`ref` role to be used with definitions and fields.
* #6316, #10804: Add domain objects to the table of contents. Patch by Adam Turner

Bugs fixed
----------
Expand Down
15 changes: 11 additions & 4 deletions sphinx/domains/javascript.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.docfields import Field, GroupedField, TypedField
from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import make_id, make_refnode
from sphinx.util.docutils import SphinxDirective, switch_source_input
from sphinx.util.nodes import make_id, make_refnode, nested_parse_with_titles
from sphinx.util.typing import OptionSpec

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -249,7 +249,7 @@ class JSModule(SphinxDirective):
:param mod_name: Module name
"""

has_content = False
has_content = True
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
Expand All @@ -261,7 +261,14 @@ def run(self) -> List[Node]:
mod_name = self.arguments[0].strip()
self.env.ref_context['js:module'] = mod_name
noindex = 'noindex' in self.options
ret: List[Node] = []

content_node: Element = nodes.section()
with switch_source_input(self.state, self.content):
# necessary so that the child nodes get the right source/line set
content_node.document = self.state.document
nested_parse_with_titles(self.state, self.content, content_node)

ret: List[Node] = [*content_node.children]
if not noindex:
domain = cast(JavaScriptDomain, self.env.get_domain('js'))

Expand Down
16 changes: 12 additions & 4 deletions sphinx/domains/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@
from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.docfields import Field, GroupedField, TypedField
from sphinx.util.docutils import SphinxDirective
from sphinx.util.docutils import SphinxDirective, switch_source_input
from sphinx.util.inspect import signature_from_str
from sphinx.util.nodes import find_pending_xref_condition, make_id, make_refnode
from sphinx.util.nodes import (find_pending_xref_condition, make_id, make_refnode,
nested_parse_with_titles)
from sphinx.util.typing import OptionSpec, TextlikeNode

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -967,7 +968,7 @@ class PyModule(SphinxDirective):
Directive to mark description of a new module.
"""

has_content = False
has_content = True
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
Expand All @@ -984,7 +985,14 @@ def run(self) -> List[Node]:
modname = self.arguments[0].strip()
noindex = 'noindex' in self.options
self.env.ref_context['py:module'] = modname
ret: List[Node] = []

content_node: Element = nodes.section()
with switch_source_input(self.state, self.content):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad to see this fixed here too. I didn't look into it too deeply, but should PyModule just use the same mechanisms as the other Py* classes (subclassing from PyObject)?

Also I suppose the docs should be updated for this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this addressed (documenting the change to py:module)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes:

image

A

# necessary so that the child nodes get the right source/line set
content_node.document = self.state.document
nested_parse_with_titles(self.state, self.content, content_node)

ret: List[Node] = [*content_node.children]
if not noindex:
# note module to the domain
node_id = make_id(self.env, self.state.document, 'module', modname)
Expand Down
97 changes: 70 additions & 27 deletions sphinx/environment/collectors/toctree.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Toctree collector for sphinx.environment."""

from typing import Any, Dict, List, Optional, Set, Tuple, Type, TypeVar, cast
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, TypeVar, Union, cast

from docutils import nodes
from docutils.nodes import Element, Node
Expand Down Expand Up @@ -54,20 +54,12 @@ def process_doc(self, app: Sphinx, doctree: nodes.document) -> None:
docname = app.env.docname
numentries = [0] # nonlocal again...

def traverse_in_section(node: Element, cls: Type[N]) -> List[N]:
"""Like traverse(), but stay within the same section."""
result: List[N] = []
if isinstance(node, cls):
result.append(node)
for child in node.children:
if isinstance(child, nodes.section):
continue
elif isinstance(child, nodes.Element):
result.extend(traverse_in_section(child, cls))
return result

def build_toc(node: Element, depth: int = 1) -> Optional[nodes.bullet_list]:
def build_toc(
node: Union[Element, Sequence[Element]],
depth: int = 1
) -> Optional[nodes.bullet_list]:
entries: List[Element] = []
memo_class = '\4\2' # sentinel, value unimportant
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 All @@ -79,13 +71,7 @@ def build_toc(node: Element, depth: int = 1) -> Optional[nodes.bullet_list]:
visitor = SphinxContentsFilter(doctree)
title.walkabout(visitor)
nodetext = visitor.get_entry_text()
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
anchorname = _make_anchor_name(sectionnode['ids'], numentries)
# make these nodes:
# list_item -> compact_paragraph -> reference
reference = nodes.reference(
Expand All @@ -97,22 +83,68 @@ def build_toc(node: Element, depth: int = 1) -> Optional[nodes.bullet_list]:
if sub_item:
item += sub_item
entries.append(item)
# Wrap items under an ``.. only::`` directive in a node for
# post-processing
elif isinstance(sectionnode, addnodes.only):
onlynode = addnodes.only(expr=sectionnode['expr'])
blist = build_toc(sectionnode, depth)
if blist:
onlynode += blist.children
entries.append(onlynode)
# check within the section for other node types
elif isinstance(sectionnode, nodes.Element):
for toctreenode in traverse_in_section(sectionnode,
addnodes.toctree):
item = toctreenode.copy()
entries.append(item)
# important: do the inventory stuff
TocTree(app.env).note(docname, toctreenode)
toctreenode: nodes.Node
for toctreenode in sectionnode.findall():
if isinstance(toctreenode, nodes.section):
continue
if isinstance(toctreenode, addnodes.toctree):
item = toctreenode.copy()
entries.append(item)
# important: do the inventory stuff
TocTree(app.env).note(docname, toctreenode)
# add object signatures within a section to the ToC
elif isinstance(toctreenode, addnodes.desc):
title = toctreenode[0]

# Skip entries with no ID (e.g. with ``:noindex:``)
if not title['ids']:
continue
# Skip if no name set. Known not to set 'fullname'
# correctly: C/C++/RST domain, option/cmdoption, envvar
full_name = title.get('fullname', '')
if not full_name:
continue

# Nest within Python classes
if 'class' in title and title['class'] == memo_class:
nested_toc = build_toc([toctreenode], depth + 1)
if nested_toc:
last_entry: nodes.Element = entries[-1]
if not isinstance(last_entry[-1], nodes.bullet_list):
last_entry.append(nested_toc)
else:
last_entry[-1].extend(nested_toc)
continue
else:
memo_class = full_name

# Cut class name from Python methods
if toctreenode.get('domain', '') == 'py' and title.get('class'):
if full_name.startswith(title['class'] + '.'):
full_name = full_name[len(title['class'] + '.'):]
if toctreenode['objtype'] in {'function', 'method'}:
full_name += '()'
anchorname = _make_anchor_name(title['ids'], numentries)
reference = nodes.reference(
'', full_name, internal=True,
refuri=docname, anchorname=anchorname)
para = addnodes.compact_paragraph('', '', reference)
entries.append(nodes.list_item('', para))

if entries:
return nodes.bullet_list('', *entries)
return None

toc = build_toc(doctree)
if toc:
app.env.tocs[docname] = toc
Expand Down Expand Up @@ -283,6 +315,17 @@ def _walk_doc(docname: str, secnum: Tuple[int, ...]) -> None:
return rewrite_needed


def _make_anchor_name(ids: List[str], num_entries: List[int]) -> str:
if not num_entries[0]:
# for the very first toc entry, don't add an anchor
# as it is the file's title anyway
anchorname = ''
else:
anchorname = '#' + ids[0]
num_entries[0] += 1
return anchorname


def setup(app: Sphinx) -> Dict[str, Any]:
app.add_env_collector(TocTreeCollector)

Expand Down
9 changes: 9 additions & 0 deletions sphinx/ext/autodoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,15 @@ def __init__(self, *args: Any) -> None:
merge_members_option(self.options)
self.__all__: Optional[Sequence[str]] = None

def add_content(self, more_content: Optional[StringList]) -> None:
old_indent = self.indent
self.indent += ' '
super().add_content(None)
self.indent = old_indent
if more_content:
for line, src in zip(more_content.data, more_content.items):
self.add_line(line, src[0], src[1])

@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
Expand Down
Empty file.
39 changes: 39 additions & 0 deletions tests/roots/test-toctree-domain-objects/domains.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
test-domain-objects
===================

.. py:module:: hello

.. py:function:: world() -> str

Prints "Hello, World!" to stdout

.. py:class:: HelloWorldPrinter

Controls printing of hello world

.. py:method:: set_language()

Sets the language of the HelloWorldPrinter instance

.. py:attribute:: output_count

Count of outputs of "Hello, World!"

.. py:method:: print_normal()
:async:
:classmethod:

Prints the normal form of "Hello, World!"

.. py:method:: print()

Prints "Hello, World!", including in the chosen language

.. py:function:: exit()
:module: sys

Quits the interpreter

.. js:function:: fetch(resource)

Fetches the given resource, returns a Promise
6 changes: 6 additions & 0 deletions tests/roots/test-toctree-domain-objects/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.. toctree::
:numbered:
:caption: Table of Contents
:name: mastertoc

domains
40 changes: 40 additions & 0 deletions tests/test_environment_toctree.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,46 @@ def test_glob(app):
assert app.env.numbered_toctrees == set()


@pytest.mark.sphinx('dummy', testroot='toctree-domain-objects')
def test_domain_objects(app):
includefiles = ['domains']

app.build()

assert app.env.toc_num_entries['index'] == 0
assert app.env.toc_num_entries['domains'] == 9
assert app.env.toctree_includes['index'] == includefiles
for file in includefiles:
assert 'index' in app.env.files_to_rebuild[file]
assert app.env.glob_toctrees == set()
assert app.env.numbered_toctrees == {'index'}

# tocs
toctree = app.env.tocs['domains']
print(toctree[0][1][1][1][0])
AA-Turner marked this conversation as resolved.
Show resolved Hide resolved
assert_node(toctree,
[bullet_list, list_item, (compact_paragraph, # [0][0]
[bullet_list, (list_item, # [0][1][0]
[list_item, # [0][1][1]
(compact_paragraph, # [0][1][1][0]
[bullet_list, (list_item, # [0][1][1][1][0]
list_item,
list_item,
list_item)])], # [0][1][1][1][3]
list_item,
list_item)])]) # [0][1][1]

assert_node(toctree[0][0],
[compact_paragraph, reference, "test-domain-objects"])

assert_node(toctree[0][1][0],
[list_item, ([compact_paragraph, reference, "world()"])])

print(toctree[0][1][1][1][3])
assert_node(toctree[0][1][1][1][3],
[list_item, ([compact_paragraph, reference, "print()"])])


@pytest.mark.sphinx('xml', testroot='toctree')
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
def test_get_toc_for(app):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_ext_autodoc_automodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_empty_all(app):
'',
'.. py:module:: target.empty_all',
'',
'docsting of empty_all module.',
' docsting of empty_all module.',
'',
]

Expand Down