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

Only create a target for object descriptions #10478

Merged
merged 32 commits into from Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
415eed4
ObjectDescription: Add option :hidden:
latosha-maltba Jun 4, 2022
46f13ec
ObjectDescription: Hide contents if :hidden: is given
latosha-maltba Jun 4, 2022
477d3f1
ObjectDescription: Record source location for targets
latosha-maltba Jun 4, 2022
30f9cc8
ObjectDescription: Replace collect_ids() with pure function
latosha-maltba Jun 4, 2022
a8ffd73
ObjectDescription: Make replace_node_with_target() a static method
latosha-maltba Jun 4, 2022
4b9116c
ObjectDescription: Factor out collect_ids() method
latosha-maltba Jun 4, 2022
61693f1
ObjectDescription: Simplify replace_node_with_target()
latosha-maltba Jun 4, 2022
f97de0b
ObjectDescription: Inline replace_node_with_target()
latosha-maltba Jun 4, 2022
54b909e
ObjectDescription: Rename :hidden: to :notypesetting:
latosha-maltba Jun 4, 2022
2a3f189
Document :notypesetting: directive option for domains
latosha-maltba Jun 4, 2022
0ebf888
Domain CPP: Update `option_spec` of superclass instead replacing it
latosha-maltba Jun 4, 2022
a6ff71c
Domain C and CPP: Drop ``:notypesetting:`` from alias object
latosha-maltba Jun 4, 2022
28633ef
Domain JavaScript: Fix order of index and target nodes
latosha-maltba Jun 4, 2022
3fdcc97
Domain Python: Fix order of index and target nodes
latosha-maltba Jun 4, 2022
fd01cd0
ObjectDescription: Do not create a target node without ids
latosha-maltba Jun 4, 2022
d975c60
ObjectDescription: Add tests for :notypesetting: option
latosha-maltba Jun 4, 2022
3e791c9
Reorder consecutive index and target nodes
latosha-maltba Jun 4, 2022
0b6ed33
Domain Python: Fix tests due to new index/target node order
latosha-maltba Jun 4, 2022
d1db8f2
Add tests for ReorderConsecutiveTargetAndIndexNodes
latosha-maltba Jun 4, 2022
fb0ad6c
CHANGES: Add entry for ``:notypesetting:``
latosha-maltba Jun 4, 2022
c3885db
Merge branch 'master' into object_description_run_5.x
AA-Turner Jul 28, 2023
6f73587
typing
AA-Turner Jul 28, 2023
2857f26
style
AA-Turner Jul 28, 2023
75977dd
notypesetting -> no-typesetting
AA-Turner Jul 28, 2023
a5a708a
Don't copy ``ObjectDescription.option_spec``
AA-Turner Jul 28, 2023
5050ec5
Add to reST domain
AA-Turner Jul 28, 2023
6f45edb
Add to JSModule and PythonModule
AA-Turner Jul 28, 2023
184773c
Updates
AA-Turner Jul 28, 2023
321816b
Quotation marks
AA-Turner Jul 28, 2023
40484b5
rename
AA-Turner Jul 28, 2023
3c46064
rename
AA-Turner Jul 28, 2023
1db991f
Update CHANGES entry.
AA-Turner Jul 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES
Expand Up @@ -29,6 +29,9 @@ Features added
* #6319: viewcode: Add :confval:`viewcode_line_numbers` to control
whether line numbers are added to rendered source code.
Patch by Ben Krikler.
* #9662: Add the ``:no-typesetting:`` option to suppress textual output
and only create a linkable anchor.
Patch by Latosha Maltba.

Bugs fixed
----------
Expand Down
24 changes: 24 additions & 0 deletions doc/usage/restructuredtext/domains.rst
Expand Up @@ -54,6 +54,9 @@ can give the directive option flag ``:nocontentsentry:``.
If you want to typeset an object description, without even making it available
for cross-referencing, you can give the directive option flag ``:noindex:``
(which implies ``:noindexentry:``).
If you do not want to typeset anything, you can give the directive option flag
``:no-typesetting:``. This can for example be used to create only a target and
index entry for later reference.
Though, note that not every directive in every domain may support these
options.

Expand All @@ -65,6 +68,10 @@ options.
The directive option ``:nocontentsentry:`` in the Python, C, C++, Javascript,
and reStructuredText domains.

.. versionadded:: 7.2
The directive option ``no-typesetting`` in the Python, C, C++, Javascript,
and reStructuredText domains.

An example using a Python domain directive::

.. py:function:: spam(eggs)
Expand All @@ -91,6 +98,23 @@ you could say ::
As you can see, both directive and role names contain the domain name and the
directive name.

The directive option ``:no-typesetting:`` can be used to create a target
(and index entry) which can later be referenced
by the roles provided by the domain.
This is particularly useful for literate programming:

.. code-block:: rst

.. py:function:: spam(eggs)
:no-typesetting:

.. code::

def spam(eggs):
pass

The function :py:func:`spam` does nothing.

.. rubric:: Default Domain

For documentation describing objects from solely one domain, authors will not
Expand Down
15 changes: 15 additions & 0 deletions sphinx/directives/__init__.py
Expand Up @@ -57,6 +57,7 @@ class ObjectDescription(SphinxDirective, Generic[ObjDescT]):
'noindex': directives.flag,
'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
'no-typesetting': directives.flag,
}

# types of doc fields that this directive handles, see sphinx.util.docfields
Expand Down Expand Up @@ -218,6 +219,7 @@ def run(self) -> list[Node]:
node['noindex'] = noindex = ('noindex' in self.options)
node['noindexentry'] = ('noindexentry' in self.options)
node['nocontentsentry'] = ('nocontentsentry' in self.options)
node['no-typesetting'] = ('no-typesetting' in self.options)
if self.domain:
node['classes'].append(self.domain)
node['classes'].append(node['objtype'])
Expand Down Expand Up @@ -270,6 +272,19 @@ def run(self) -> list[Node]:
DocFieldTransformer(self).transform_all(contentnode)
self.env.temp_data['object'] = None
self.after_content()

if node['no-typesetting']:
# Attempt to return the index node, and a new target node
# containing all the ids of this node and its children.
# If ``:noindex:`` is set, or there are no ids on the node
# or any of its children, then just return the index node,
# as Docutils expects a target node to have at least one id.
if node_ids := [node_id for el in node.findall(nodes.Element)
for node_id in el.get('ids', ())]:
target_node = nodes.target(ids=node_ids)
self.set_source_info(target_node)
return [self.indexnode, target_node]
return [self.indexnode]
return [self.indexnode, node]


Expand Down
1 change: 1 addition & 0 deletions sphinx/domains/c.py
Expand Up @@ -3166,6 +3166,7 @@ class CObject(ObjectDescription[ASTDeclaration]):
option_spec: OptionSpec = {
'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
'no-typesetting': directives.flag,
'single-line-parameter-list': directives.flag,
}

Expand Down
1 change: 1 addition & 0 deletions sphinx/domains/cpp.py
Expand Up @@ -7214,6 +7214,7 @@ class CPPObject(ObjectDescription[ASTDeclaration]):
option_spec: OptionSpec = {
'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
'no-typesetting': directives.flag,
'tparam-line-spec': directives.flag,
'single-line-parameter-list': directives.flag,
}
Expand Down
9 changes: 6 additions & 3 deletions sphinx/domains/javascript.py
Expand Up @@ -46,6 +46,7 @@ class JSObject(ObjectDescription[tuple[str, str]]):
'noindex': directives.flag,
'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
'no-typesetting': directives.flag,
'single-line-parameter-list': directives.flag,
}

Expand Down Expand Up @@ -293,6 +294,7 @@ class JSModule(SphinxDirective):
option_spec: OptionSpec = {
'noindex': directives.flag,
'nocontentsentry': directives.flag,
'no-typesetting': directives.flag,
}

def run(self) -> list[Node]:
Expand All @@ -316,12 +318,13 @@ def run(self) -> list[Node]:
domain.note_object(mod_name, 'module', node_id,
location=(self.env.docname, self.lineno))

target = nodes.target('', '', ids=[node_id], ismod=True)
self.state.document.note_explicit_target(target)
ret.append(target)
# The node order is: index node first, then target node
indextext = _('%s (module)') % mod_name
inode = addnodes.index(entries=[('single', indextext, node_id, '', None)])
ret.append(inode)
target = nodes.target('', '', ids=[node_id], ismod=True)
self.state.document.note_explicit_target(target)
ret.append(target)
ret.extend(content_node.children)
return ret

Expand Down
5 changes: 4 additions & 1 deletion sphinx/domains/python.py
Expand Up @@ -662,6 +662,7 @@ class PyObject(ObjectDescription[tuple[str, str]]):
'noindex': directives.flag,
'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
'no-typesetting': directives.flag,
'single-line-parameter-list': directives.flag,
'single-line-type-parameter-list': directives.flag,
'module': directives.unchanged,
Expand Down Expand Up @@ -1262,6 +1263,7 @@ class PyModule(SphinxDirective):
'synopsis': lambda x: x,
'noindex': directives.flag,
'nocontentsentry': directives.flag,
'no-typesetting': directives.flag,
'deprecated': directives.flag,
}

Expand Down Expand Up @@ -1294,10 +1296,11 @@ def run(self) -> list[Node]:

# the platform and synopsis aren't printed; in fact, they are only
# used in the modindex currently
ret.append(target)
indextext = f'module; {modname}'
inode = addnodes.index(entries=[('pair', indextext, node_id, '', None)])
# The node order is: index node first, then target node.
ret.append(inode)
ret.append(target)
ret.extend(content_node.children)
return ret

Expand Down
1 change: 1 addition & 0 deletions sphinx/domains/rst.py
Expand Up @@ -37,6 +37,7 @@ class ReSTMarkup(ObjectDescription[str]):
'noindex': directives.flag,
'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
'no-typesetting': directives.flag,
}

def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None:
Expand Down
72 changes: 72 additions & 0 deletions sphinx/transforms/__init__.py
Expand Up @@ -414,6 +414,77 @@ def apply(self, **kwargs: Any) -> None:
)


class ReorderConsecutiveTargetAndIndexNodes(SphinxTransform):
"""Index nodes interspersed between target nodes prevent other
Transformations from combining those target nodes,
e.g. ``PropagateTargets``. This transformation reorders them:

Given the following ``document`` as input::

<document>
<target ids="id1" ...>
<index entries="...1...">
<target ids="id2" ...>
<target ids="id3" ...>
<index entries="...2...">
<target ids="id4" ...>

The transformed result will be::

<document>
<index entries="...1...">
<index entries="...2...">
<target ids="id1" ...>
<target ids="id2" ...>
<target ids="id3" ...>
<target ids="id4" ...>
"""

# This transform MUST run before ``PropagateTargets``.
default_priority = 220

def apply(self, **kwargs: Any) -> None:
for target in self.document.findall(nodes.target):
_reorder_index_target_nodes(target)


def _reorder_index_target_nodes(start_node: nodes.target) -> None:
"""Sort target and index nodes.

Find all consecutive target and index nodes starting from ``start_node``,
and move all index nodes to before the first target node.
"""
nodes_to_reorder: list[nodes.target | addnodes.index] = []

# Note that we cannot use 'condition' to filter,
# as we want *consecutive* target & index nodes.
node: nodes.Node
for node in start_node.findall(descend=False, siblings=True):
if isinstance(node, (nodes.target, addnodes.index)):
nodes_to_reorder.append(node)
continue
break # must be a consecutive run of target or index nodes

if len(nodes_to_reorder) < 2:
return # Nothing to reorder

parent = nodes_to_reorder[0].parent
if parent == nodes_to_reorder[-1].parent:
first_idx = parent.index(nodes_to_reorder[0])
last_idx = parent.index(nodes_to_reorder[-1])
if first_idx + len(nodes_to_reorder) - 1 == last_idx:
parent[first_idx:last_idx + 1] = sorted(nodes_to_reorder, key=_sort_key)


def _sort_key(node: nodes.Node) -> int:
# Must be a stable sort.
if isinstance(node, addnodes.index):
return 0
if isinstance(node, nodes.target):
return 1
raise ValueError(f'_sort_key called with unexpected node type {type(node)!r}')


def setup(app: Sphinx) -> dict[str, Any]:
app.add_transform(ApplySourceWorkaround)
app.add_transform(ExtraTranslatableNodes)
Expand All @@ -430,6 +501,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_transform(DoctreeReadEvent)
app.add_transform(ManpageLink)
app.add_transform(GlossarySorter)
app.add_transform(ReorderConsecutiveTargetAndIndexNodes)

return {
'version': 'builtin',
Expand Down
108 changes: 108 additions & 0 deletions tests/test_directives_no_typesetting.py
@@ -0,0 +1,108 @@
"""Tests the directives"""

import pytest
from docutils import nodes

from sphinx import addnodes
from sphinx.testing import restructuredtext
from sphinx.testing.util import assert_node

DOMAINS = [
# directive, noindex, noindexentry, signature of f, signature of g, index entry of g
('c:function', False, True, 'void f()', 'void g()', ('single', 'g (C function)', 'c.g', '', None)),
('cpp:function', False, True, 'void f()', 'void g()', ('single', 'g (C++ function)', '_CPPv41gv', '', None)),
('js:function', True, True, 'f()', 'g()', ('single', 'g() (built-in function)', 'g', '', None)),
('py:function', True, True, 'f()', 'g()', ('pair', 'built-in function; g()', 'g', '', None)),
('rst:directive', True, False, 'f', 'g', ('single', 'g (directive)', 'directive-g', '', None)),
('cmdoption', True, False, 'f', 'g', ('pair', 'command line option; g', 'cmdoption-arg-g', '', None)),
('envvar', True, False, 'f', 'g', ('single', 'environment variable; g', 'envvar-g', '', None)),
]


@pytest.mark.parametrize(('directive', 'noindex', 'noindexentry', 'sig_f', 'sig_g', 'index_g'), DOMAINS)
def test_object_description_no_typesetting(app, directive, noindex, noindexentry, sig_f, sig_g, index_g):
text = (f'.. {directive}:: {sig_f}\n'
f' :no-typesetting:\n')
doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index, nodes.target))


@pytest.mark.parametrize(('directive', 'noindex', 'noindexentry', 'sig_f', 'sig_g', 'index_g'), DOMAINS)
def test_object_description_no_typesetting_twice(app, directive, noindex, noindexentry, sig_f, sig_g, index_g):
text = (f'.. {directive}:: {sig_f}\n'
f' :no-typesetting:\n'
f'.. {directive}:: {sig_g}\n'
f' :no-typesetting:\n')
doctree = restructuredtext.parse(app, text)
# Note that all index nodes come before the target nodes
assert_node(doctree, (addnodes.index, addnodes.index, nodes.target, nodes.target))


@pytest.mark.parametrize(('directive', 'noindex', 'noindexentry', 'sig_f', 'sig_g', 'index_g'), DOMAINS)
def test_object_description_no_typesetting_noindex_orig(app, directive, noindex, noindexentry, sig_f, sig_g, index_g):
if not noindex:
pytest.skip(f'{directive} does not support :noindex: option')
text = (f'.. {directive}:: {sig_f}\n'
f' :noindex:\n'
f'.. {directive}:: {sig_g}\n')
doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index, addnodes.desc, addnodes.index, addnodes.desc))
assert_node(doctree[2], addnodes.index, entries=[index_g])


@pytest.mark.parametrize(('directive', 'noindex', 'noindexentry', 'sig_f', 'sig_g', 'index_g'), DOMAINS)
def test_object_description_no_typesetting_noindex(app, directive, noindex, noindexentry, sig_f, sig_g, index_g):
if not noindex:
pytest.skip(f'{directive} does not support :noindex: option')
text = (f'.. {directive}:: {sig_f}\n'
f' :noindex:\n'
f' :no-typesetting:\n'
f'.. {directive}:: {sig_g}\n'
f' :no-typesetting:\n')
doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index, addnodes.index, nodes.target))
assert_node(doctree[0], addnodes.index, entries=[])
assert_node(doctree[1], addnodes.index, entries=[index_g])


@pytest.mark.parametrize(('directive', 'noindex', 'noindexentry', 'sig_f', 'sig_g', 'index_g'), DOMAINS)
def test_object_description_no_typesetting_noindexentry(app, directive, noindex, noindexentry, sig_f, sig_g, index_g):
if not noindexentry:
pytest.skip(f'{directive} does not support :noindexentry: option')
text = (f'.. {directive}:: {sig_f}\n'
f' :noindexentry:\n'
f' :no-typesetting:\n'
f'.. {directive}:: {sig_g}\n'
f' :no-typesetting:\n')
doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index, addnodes.index, nodes.target, nodes.target))
assert_node(doctree[0], addnodes.index, entries=[])
assert_node(doctree[1], addnodes.index, entries=[index_g])


@pytest.mark.parametrize(('directive', 'noindex', 'noindexentry', 'sig_f', 'sig_g', 'index_g'), DOMAINS)
def test_object_description_no_typesetting_code(app, directive, noindex, noindexentry, sig_f, sig_g, index_g):
text = (f'.. {directive}:: {sig_f}\n'
f' :no-typesetting:\n'
f'.. {directive}:: {sig_g}\n'
f' :no-typesetting:\n'
f'.. code::\n'
f'\n'
f' code\n')
doctree = restructuredtext.parse(app, text)
# Note that all index nodes come before the targets
assert_node(doctree, (addnodes.index, addnodes.index, nodes.target, nodes.target, nodes.literal_block))


@pytest.mark.parametrize(('directive', 'noindex', 'noindexentry', 'sig_f', 'sig_g', 'index_g'), DOMAINS)
def test_object_description_no_typesetting_heading(app, directive, noindex, noindexentry, sig_f, sig_g, index_g):
text = (f'.. {directive}:: {sig_f}\n'
f' :no-typesetting:\n'
f'.. {directive}:: {sig_g}\n'
f' :no-typesetting:\n'
f'\n'
f'Heading\n'
f'=======\n')
doctree = restructuredtext.parse(app, text)
# Note that all index nodes come before the targets and the heading is floated before those.
assert_node(doctree, (nodes.title, addnodes.index, addnodes.index, nodes.target, nodes.target))
8 changes: 4 additions & 4 deletions tests/test_domain_js.py
Expand Up @@ -177,11 +177,11 @@ def test_get_full_qualified_name():
def test_js_module(app):
text = ".. js:module:: sphinx"
doctree = restructuredtext.parse(app, text)
assert_node(doctree, (nodes.target,
addnodes.index))
assert_node(doctree[0], nodes.target, ids=["module-sphinx"])
assert_node(doctree[1], addnodes.index,
assert_node(doctree, (addnodes.index,
nodes.target))
assert_node(doctree[0], addnodes.index,
entries=[("single", "sphinx (module)", "module-sphinx", "", None)])
assert_node(doctree[1], nodes.target, ids=["module-sphinx"])


def test_js_function(app):
Expand Down