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 17 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
14 changes: 14 additions & 0 deletions doc/usage/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,20 @@ General configuration
:term:`object` names (for object types where a "module" of some kind is
defined), e.g. for :rst:dir:`py:function` directives. Default is ``True``.

.. confval:: toc_object_entries_parents

A string that determines how domain objects (e.g. functions, classes,
attributes, etc.) are displayed in their table of contents entry.

The default (``hide``) is to only show the name of the element without any
parents, i.e. showing ``function()``.
To show the name and immediate parent (i.e. ``Class.function()``), use the
``immediate`` setting.
To show the fully-qualified name for the object and display all parents
(i.e. ``module.Class.function()``), use the ``all`` setting.

.. versionadded:: 5.2

.. confval:: show_authors

A boolean that decides whether :rst:dir:`codeauthor` and
Expand Down
13 changes: 11 additions & 2 deletions doc/usage/restructuredtext/domains.rst
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,15 @@ declarations:

This directive marks the beginning of the description of a module (or package
submodule, in which case the name should be fully qualified, including the
package name). It does not create content (like e.g. :rst:dir:`py:class`
does).
package name). A description of the module such as the docstring can be
placed in the body of the directive.

This directive will also cause an entry in the global module index.

.. versionchanged:: 5.2

Module directives support body content.

.. rubric:: options

.. rst:directive:option:: platform: platforms
Expand All @@ -165,6 +169,8 @@ declarations:
Mark a module as deprecated; it will be designated as such in various
locations then.



.. rst:directive:: .. py:currentmodule:: name

This directive tells Sphinx that the classes, functions etc. documented from
Expand Down Expand Up @@ -1826,6 +1832,9 @@ The JavaScript domain (name **js**) provides the following directives:
current module name.

.. versionadded:: 1.6
.. versionchanged:: 5.2

Module directives support body content.

.. rst:directive:: .. js:function:: name(signature)

Expand Down
2 changes: 2 additions & 0 deletions sphinx/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ class Config:
'default_role': (None, 'env', [str]),
'add_function_parentheses': (True, 'env', []),
'add_module_names': (True, 'env', []),
'toc_object_entries_parents': ('hide', 'env',
ENUM('all', 'immediate', 'hide')),
'trim_footnote_reference_space': (False, 'env', []),
'show_authors': (False, 'env', []),
'pygments_style': (None, 'html', [str]),
Expand Down
11 changes: 11 additions & 0 deletions sphinx/directives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class ObjectDescription(SphinxDirective, Generic[T]):

# Warning: this might be removed in future version. Don't touch this from extensions.
_doc_field_type_map: Dict[str, Tuple[Field, bool]] = {}
# Don't touch this from extensions, will be removed without notice
_toc_parents: Tuple[str, ...] = ()

def get_field_type_map(self) -> Dict[str, Tuple[Field, bool]]:
if self._doc_field_type_map == {}:
Expand Down Expand Up @@ -131,6 +133,9 @@ def after_content(self) -> None:
"""
pass

def _table_of_contents_name(self, node: addnodes.desc) -> str:
return ''

def run(self) -> List[Node]:
"""
Main directive entry function, called by docutils upon encountering the
Expand Down Expand Up @@ -203,6 +208,12 @@ def run(self) -> List[Node]:

contentnode = addnodes.desc_content()
node.append(contentnode)

# Private attributes for ToC generation. Will be modified or removed
# without notice.
node['_toc_parents'] = self._toc_parents
node['_toc_name'] = self._table_of_contents_name(node)

if self.names:
# needed for association of version{added,changed} directives
self.env.temp_data['object'] = self.names[0]
Expand Down
36 changes: 32 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 @@ -85,6 +85,11 @@ def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]
signode['object'] = prefix
signode['fullname'] = fullname

if mod_name:
self._toc_parents = (mod_name, *fullname.split('.'))
else:
self._toc_parents = tuple(fullname.split('.'))

display_prefix = self.get_display_prefix()
if display_prefix:
signode += addnodes.desc_annotation('', '', *display_prefix)
Expand Down Expand Up @@ -201,6 +206,22 @@ def make_old_id(self, fullname: str) -> str:
"""
return fullname.replace('$', '_S_')

def _table_of_contents_name(self, node: addnodes.desc) -> str:
if not self._toc_parents:
return ''

config = self.env.app.config
*parents, name = self._toc_parents
if (config.add_function_parentheses
and node['objtype'] in {'function', 'method'}): # NoQA: W503
name += '()'

if config.toc_object_entries_parents == 'all':
return '.'.join(parents + [name])
if config.toc_object_entries_parents == 'immediate' and len(parents):
return f'{parents[-1]}.{name}'
return name


class JSCallable(JSObject):
"""Description of a JavaScript function, method or constructor."""
Expand Down Expand Up @@ -249,7 +270,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 +282,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
37 changes: 33 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 @@ -508,6 +509,11 @@ def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]
signode['class'] = classname
signode['fullname'] = fullname

if modname:
self._toc_parents = (modname, *fullname.split('.'))
else:
self._toc_parents = tuple(fullname.split('.'))

sig_prefix = self.get_signature_prefix(sig)
if sig_prefix:
if type(sig_prefix) is str:
Expand Down Expand Up @@ -640,6 +646,22 @@ def after_content(self) -> None:
else:
self.env.ref_context.pop('py:module')

def _table_of_contents_name(self, node: addnodes.desc) -> str:
if not self._toc_parents:
return ''

config = self.env.app.config
*parents, name = self._toc_parents
if (config.add_function_parentheses
and node['objtype'] in {'function', 'method'}): # NoQA: W503
name += '()'

if config.toc_object_entries_parents == 'all':
return '.'.join(parents + [name])
if config.toc_object_entries_parents == 'immediate' and len(parents):
return f'{parents[-1]}.{name}'
return name


class PyFunction(PyObject):
"""Description of a function."""
Expand Down Expand Up @@ -967,7 +989,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 +1006,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
89 changes: 62 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_parents = () # 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,60 @@ 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):
# Skip if no name set
if not toctreenode.get('_toc_name', ''):
continue
# Skip entries with no ID (e.g. with :noindex: set)
ids = toctreenode[0]['ids']
if not ids:
continue

# Nest children within parents
*parents, _ = toctreenode['_toc_parents']
if parents and tuple(parents) == memo_parents:
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_parents = toctreenode['_toc_parents']

anchorname = _make_anchor_name(ids, numentries)
reference = nodes.reference(
'', toctreenode['_toc_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 +307,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
Loading