From 415eed4f5e75d551f7437c20f4ebc0321db928dd Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:48 +0000 Subject: [PATCH 01/31] ObjectDescription: Add option :hidden: Add option :hidden: to ObjectDescription. Currently, this option has no effect except being stored as attribute in the resulting node. --- sphinx/directives/__init__.py | 2 ++ sphinx/domains/c.py | 12 ++++++++---- sphinx/domains/cpp.py | 6 ++++-- sphinx/domains/javascript.py | 6 +++--- sphinx/domains/python.py | 6 +++--- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 2656cc99b79..b24f307c90f 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -51,6 +51,7 @@ class ObjectDescription(SphinxDirective, Generic[T]): final_argument_whitespace = True option_spec: OptionSpec = { 'noindex': directives.flag, + 'hidden': directives.flag, } # types of doc fields that this directive handles, see sphinx.util.docfields @@ -161,6 +162,7 @@ def run(self) -> List[Node]: # 'desctype' is a backwards compatible attribute node['objtype'] = node['desctype'] = self.objtype node['noindex'] = noindex = ('noindex' in self.options) + node['hidden'] = 'hidden' in self.options if self.domain: node['classes'].append(self.domain) node['classes'].append(node['objtype']) diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index ea29b94b7b3..c07ccc5a1ea 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -3141,9 +3141,11 @@ class CObject(ObjectDescription[ASTDeclaration]): Description of a C language object. """ - option_spec: OptionSpec = { + option_spec: OptionSpec = ObjectDescription.option_spec.copy() + option_spec.update({ 'noindexentry': directives.flag, - } + }) + del option_spec['noindex'] # is in ObjectDescription but doesn't make sense here def _add_enumerator_to_parent(self, ast: ASTDeclaration) -> None: assert ast.objectType == 'enumerator' @@ -3615,10 +3617,12 @@ def apply(self, **kwargs: Any) -> None: class CAliasObject(ObjectDescription): - option_spec: OptionSpec = { + option_spec: OptionSpec = ObjectDescription.option_spec.copy() + option_spec.update({ 'maxdepth': directives.nonnegative_int, 'noroot': directives.flag, - } + }) + del option_spec['noindex'] # is in ObjectDescription but doesn't make sense here def run(self) -> List[Node]: """ diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 98a89594f27..d4ad8c9ef86 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -7083,10 +7083,12 @@ class CPPObject(ObjectDescription[ASTDeclaration]): can_collapse=True), ] - option_spec: OptionSpec = { + option_spec: OptionSpec = ObjectDescription.option_spec.copy() + option_spec.update({ 'noindexentry': directives.flag, 'tparam-line-spec': directives.flag, - } + }) + del option_spec['noindex'] # is in ObjectDescription but doesn't make sense here def _add_enumerator_to_parent(self, ast: ASTDeclaration) -> None: assert ast.objectType == 'enumerator' diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index 60ea31e94ac..afc1ab322e6 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -37,10 +37,10 @@ class JSObject(ObjectDescription[Tuple[str, str]]): #: based on directive nesting allow_nesting = False - option_spec: OptionSpec = { - 'noindex': directives.flag, + option_spec: OptionSpec = ObjectDescription.option_spec.copy() + option_spec.update({ 'noindexentry': directives.flag, - } + }) def get_display_prefix(self) -> List[Node]: #: what is displayed right before the documentation entry diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index a634a51d2b2..0bbe2d6e702 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -421,13 +421,13 @@ class PyObject(ObjectDescription[Tuple[str, str]]): :cvar allow_nesting: Class is an object that allows for nested namespaces :vartype allow_nesting: bool """ - option_spec: OptionSpec = { - 'noindex': directives.flag, + option_spec: OptionSpec = ObjectDescription.option_spec.copy() + option_spec.update({ 'noindexentry': directives.flag, 'module': directives.unchanged, 'canonical': directives.unchanged, 'annotation': directives.unchanged, - } + }) doc_field_types = [ PyTypedField('parameter', label=_('Parameters'), From 46f13eca8a749b34d9496508ae0a13d91c056182 Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 02/31] ObjectDescription: Hide contents if :hidden: is given Do not produce any output only a target node if option :hidden: is given. In this case all nodes representing this ObjectDescription are replaced by a single target where the target gets assigned all ids of the replaced nodes. --- sphinx/directives/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index b24f307c90f..8c04ea54efc 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -205,8 +205,29 @@ def run(self) -> List[Node]: DocFieldTransformer(self).transform_all(contentnode) self.env.temp_data['object'] = None self.after_content() + + if node['hidden']: + node = self.replace_node_with_target(node) + return [self.indexnode, node] + # TODO: This method does not need access to self, make a (non-class) function out of it? + def replace_node_with_target(self, node: nodes.Node) -> nodes.target: + def collect_ids(node: nodes.Node, ids: List[str]) -> List[str]: + if not isinstance(node, nodes.Element): + return + theseIds = node.get('ids') + if theseIds: + ids.extend(theseIds) + for c in node.children: + collect_ids(c, ids) + + ids: List[str] = [] + collect_ids(node, ids) + target_node = nodes.target() + target_node['ids'] = ids + return target_node + class DefaultRole(SphinxDirective): """ From 477d3f1595a6e648288df7df49813fd400995154 Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 03/31] ObjectDescription: Record source location for targets Record the source information (class attributes source and line) when replacing an object description node with a target node. --- sphinx/directives/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 8c04ea54efc..da75181ffe7 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -208,6 +208,7 @@ def run(self) -> List[Node]: if node['hidden']: node = self.replace_node_with_target(node) + self.set_source_info(node) return [self.indexnode, node] From 30f9cc8592c58ee939d91600db157ad72c7c1b1e Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 04/31] ObjectDescription: Replace collect_ids() with pure function The collect_ids() function uses the its second parameter ``ids: List[str]`` as in- and output parameter and always returns None. Rewrite the function to become pure and side effect free: return the list of ids as the return value not as an output parameter. --- sphinx/directives/__init__.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index da75181ffe7..0cb96672de7 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -214,17 +214,17 @@ def run(self) -> List[Node]: # TODO: This method does not need access to self, make a (non-class) function out of it? def replace_node_with_target(self, node: nodes.Node) -> nodes.target: - def collect_ids(node: nodes.Node, ids: List[str]) -> List[str]: - if not isinstance(node, nodes.Element): - return - theseIds = node.get('ids') - if theseIds: - ids.extend(theseIds) - for c in node.children: - collect_ids(c, ids) - - ids: List[str] = [] - collect_ids(node, ids) + + def collect_ids(node: nodes.Node) -> List[str]: + if isinstance(node, nodes.Element): + ids: List[str] = node.get('ids', []) + for c in node.children: + ids.extend(collect_ids(c)) + return ids + else: + return [] + + ids: List[str] = collect_ids(node) target_node = nodes.target() target_node['ids'] = ids return target_node From a8ffd7392ad948a8c1bdd76f27efb5569cd92161 Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 05/31] ObjectDescription: Make replace_node_with_target() a static method replace_node_with_target() does not access the self-instance. Thus, declare it as a static method. --- sphinx/directives/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 0cb96672de7..03f291563cc 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -207,13 +207,13 @@ def run(self) -> List[Node]: self.after_content() if node['hidden']: - node = self.replace_node_with_target(node) + node = self.__class__.replace_node_with_target(node) self.set_source_info(node) return [self.indexnode, node] - # TODO: This method does not need access to self, make a (non-class) function out of it? - def replace_node_with_target(self, node: nodes.Node) -> nodes.target: + @staticmethod + def replace_node_with_target(node: nodes.Node) -> nodes.target: def collect_ids(node: nodes.Node) -> List[str]: if isinstance(node, nodes.Element): @@ -224,7 +224,7 @@ def collect_ids(node: nodes.Node) -> List[str]: else: return [] - ids: List[str] = collect_ids(node) + ids: List[str] = ObjectDescription.collect_ids(node) target_node = nodes.target() target_node['ids'] = ids return target_node From 4b9116c0c6c28f5da80ebb9e7df0871837410b7a Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 06/31] ObjectDescription: Factor out collect_ids() method Move the collect_ids() method from inside the replace_node_with_target() into the class scope (as static method) since it does not need the closure of the surrounding replace_node_with_target() method. This also gives a potential speed boost. --- sphinx/directives/__init__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 03f291563cc..67e4fed585f 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -214,21 +214,21 @@ def run(self) -> List[Node]: @staticmethod def replace_node_with_target(node: nodes.Node) -> nodes.target: - - def collect_ids(node: nodes.Node) -> List[str]: - if isinstance(node, nodes.Element): - ids: List[str] = node.get('ids', []) - for c in node.children: - ids.extend(collect_ids(c)) - return ids - else: - return [] - ids: List[str] = ObjectDescription.collect_ids(node) target_node = nodes.target() target_node['ids'] = ids return target_node + @staticmethod + def collect_ids(node: nodes.Node) -> List[str]: + if isinstance(node, nodes.Element): + ids: List[str] = node.get('ids', []) + for c in node.children: + ids.extend(ObjectDescription.collect_ids(c)) + return ids + else: + return [] + class DefaultRole(SphinxDirective): """ From 61693f1f4773cf4d39f28c20a35c185cb243ff48 Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 07/31] ObjectDescription: Simplify replace_node_with_target() --- sphinx/directives/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 67e4fed585f..1bbafde5dbb 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -214,10 +214,7 @@ def run(self) -> List[Node]: @staticmethod def replace_node_with_target(node: nodes.Node) -> nodes.target: - ids: List[str] = ObjectDescription.collect_ids(node) - target_node = nodes.target() - target_node['ids'] = ids - return target_node + return nodes.target(ids=ObjectDescription.collect_ids(node)) @staticmethod def collect_ids(node: nodes.Node) -> List[str]: From f97de0bb0a10e70e4739e9dda5bad5a29ac0d0eb Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 08/31] ObjectDescription: Inline replace_node_with_target() Inline the one-line method replace_node_with_target() into its only call site. Since there are no call sites left, remove the method afterwards. --- sphinx/directives/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 1bbafde5dbb..a7915372a07 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -207,15 +207,13 @@ def run(self) -> List[Node]: self.after_content() if node['hidden']: - node = self.__class__.replace_node_with_target(node) + # replace the node with a target node containing all the ids of + # this node and its children. + node = nodes.target(ids=self.__class__.collect_ids(node)) self.set_source_info(node) return [self.indexnode, node] - @staticmethod - def replace_node_with_target(node: nodes.Node) -> nodes.target: - return nodes.target(ids=ObjectDescription.collect_ids(node)) - @staticmethod def collect_ids(node: nodes.Node) -> List[str]: if isinstance(node, nodes.Element): From 54b909ea2824d40c101e0e6ae90554ce80c7abb0 Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 09/31] ObjectDescription: Rename :hidden: to :notypesetting: --- sphinx/directives/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index a7915372a07..684638dfbf4 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -51,7 +51,7 @@ class ObjectDescription(SphinxDirective, Generic[T]): final_argument_whitespace = True option_spec: OptionSpec = { 'noindex': directives.flag, - 'hidden': directives.flag, + 'notypesetting': directives.flag, } # types of doc fields that this directive handles, see sphinx.util.docfields @@ -162,7 +162,7 @@ def run(self) -> List[Node]: # 'desctype' is a backwards compatible attribute node['objtype'] = node['desctype'] = self.objtype node['noindex'] = noindex = ('noindex' in self.options) - node['hidden'] = 'hidden' in self.options + node['notypesetting'] = 'notypesetting' in self.options if self.domain: node['classes'].append(self.domain) node['classes'].append(node['objtype']) @@ -206,7 +206,7 @@ def run(self) -> List[Node]: self.env.temp_data['object'] = None self.after_content() - if node['hidden']: + if node['notypesetting']: # replace the node with a target node containing all the ids of # this node and its children. node = nodes.target(ids=self.__class__.collect_ids(node)) From 2a3f189d7a7e7d803e355d9821acbb72ad151190 Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 10/31] Document :notypesetting: directive option for domains --- doc/usage/restructuredtext/domains.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index 30bde8ea138..d876a5072bc 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -50,6 +50,9 @@ give the directive option flag ``:noindexentry:``. 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 +``:notypesetting:``. 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. @@ -57,6 +60,9 @@ options. The directive option ``noindexentry`` in the Python, C, C++, and Javascript domains. +.. versionadded:: 5.1 + The directive option ``notypesetting``. + An example using a Python domain directive:: .. py:function:: spam(eggs) @@ -83,6 +89,19 @@ you could say :: As you can see, both directive and role names contain the domain name and the directive name. +The directive option ``:notypesetting:`` 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:: + + .. py:function:: spam(eggs) + :notypesetting: + .. 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 From 0ebf8884f90672f73a7450d0502c7f1aff2504d0 Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 11/31] Domain CPP: Update `option_spec` of superclass instead replacing it Instead of overriding the `option_spec` variable copy the `option_spec` of the superclass and update it. This way changes in the superclass propagate to its children. This is in line how other classes use "`option_spec`-inheritance". --- sphinx/domains/cpp.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index d4ad8c9ef86..669d2f8ba00 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -7606,10 +7606,12 @@ def apply(self, **kwargs: Any) -> None: class CPPAliasObject(ObjectDescription): - option_spec: OptionSpec = { + option_spec: OptionSpec = ObjectDescription.option_spec.copy() + option_spec.update({ 'maxdepth': directives.nonnegative_int, 'noroot': directives.flag, - } + }) + del option_spec['noindex'] # is in ObjectDescription but doesn't make sense here def run(self) -> List[Node]: """ From a6ff71cf3d55f21c20ec7b33759233c45428d22b Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 12/31] Domain C and CPP: Drop ``:notypesetting:`` from alias object The alias objects override the run() method and thus do not support ``:notypesetting:`` option. --- sphinx/domains/c.py | 1 + sphinx/domains/cpp.py | 1 + 2 files changed, 2 insertions(+) diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index c07ccc5a1ea..bc2e6a3d6dc 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -3623,6 +3623,7 @@ class CAliasObject(ObjectDescription): 'noroot': directives.flag, }) del option_spec['noindex'] # is in ObjectDescription but doesn't make sense here + del option_spec['notypesetting'] # is in ObjectDescription but doesn't make sense here def run(self) -> List[Node]: """ diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 669d2f8ba00..f478196dd87 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -7612,6 +7612,7 @@ class CPPAliasObject(ObjectDescription): 'noroot': directives.flag, }) del option_spec['noindex'] # is in ObjectDescription but doesn't make sense here + del option_spec['notypesetting'] # is in ObjectDescription but doesn't make sense here def run(self) -> List[Node]: """ From 28633ef36352824c22ebcdc7400c2a7982bcec08 Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 13/31] Domain JavaScript: Fix order of index and target nodes Correct node order generated by the .. js:module:: directive: When generating an index entry, the index node must come before the target node. --- sphinx/domains/javascript.py | 7 ++++--- tests/test_domain_js.py | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index afc1ab322e6..3b88f9bea36 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -272,12 +272,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) return ret def make_old_id(self, modname: str) -> str: diff --git a/tests/test_domain_js.py b/tests/test_domain_js.py index 465fef328cd..bfc27ec9f29 100644 --- a/tests/test_domain_js.py +++ b/tests/test_domain_js.py @@ -166,11 +166,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): From 3fdcc97a8203c17587cd8f56a2967f7131d60dc7 Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 14/31] Domain Python: Fix order of index and target nodes Correct node order generated by the .. py:module:: directive: When generating an index entry, the index node must come before the target node. --- sphinx/domains/python.py | 3 ++- tests/test_domain_py.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 0bbe2d6e702..81144021179 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -999,10 +999,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 = '%s; %s' % (pairindextypes['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) return ret def make_old_id(self, name: str) -> str: diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 014067e8459..02789069c4e 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -615,8 +615,8 @@ def test_pydata(app): " :type: int\n") domain = app.env.get_domain('py') doctree = restructuredtext.parse(app, text) - assert_node(doctree, (nodes.target, - addnodes.index, + assert_node(doctree, (addnodes.index, + nodes.target, addnodes.index, [desc, ([desc_signature, ([desc_addname, "example."], [desc_name, "var"], @@ -640,8 +640,8 @@ def test_pyfunction(app): [desc, ([desc_signature, ([desc_name, "func1"], [desc_parameterlist, ()])], [desc_content, ()])], - nodes.target, addnodes.index, + nodes.target, addnodes.index, [desc, ([desc_signature, ([desc_annotation, ([desc_sig_keyword, 'async'], desc_sig_space)], @@ -651,7 +651,7 @@ def test_pyfunction(app): [desc_content, ()])])) assert_node(doctree[0], addnodes.index, entries=[('pair', 'built-in function; func1()', 'func1', '', None)]) - assert_node(doctree[3], addnodes.index, + assert_node(doctree[2], addnodes.index, entries=[('pair', 'module; example', 'module-example', '', None)]) assert_node(doctree[4], addnodes.index, entries=[('single', 'func2() (in module example)', 'example.func2', '', None)]) @@ -1010,8 +1010,8 @@ def test_info_field_list(app): doctree = restructuredtext.parse(app, text) print(doctree) - assert_node(doctree, (nodes.target, - addnodes.index, + assert_node(doctree, (addnodes.index, + nodes.target, addnodes.index, [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], [desc_addname, "example."], @@ -1101,8 +1101,8 @@ def test_info_field_list_piped_type(app): doctree = restructuredtext.parse(app, text) assert_node(doctree, - (nodes.target, - addnodes.index, + (addnodes.index, + nodes.target, addnodes.index, [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], [desc_addname, "example."], @@ -1135,8 +1135,8 @@ def test_info_field_list_Literal(app): doctree = restructuredtext.parse(app, text) assert_node(doctree, - (nodes.target, - addnodes.index, + (addnodes.index, + nodes.target, addnodes.index, [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], [desc_addname, "example."], From fd01cd02e09d97338d5b9170636e3e482955c8ca Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 15/31] ObjectDescription: Do not create a target node without ids It might be that a object description has no ids associated with it, e.g. if the option :noindex: is passed. In this case we would replace the object description node with a target node, which has no ids. This breaks docutils assumption about a target node and leads not errors. Therefore, do only create a target node if we have ids to work with. --- sphinx/directives/__init__.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 684638dfbf4..5a9ec2e5818 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -206,13 +206,22 @@ def run(self) -> List[Node]: self.env.temp_data['object'] = None self.after_content() - if node['notypesetting']: + ret: List[nodes.Node] = [] + ret.append(self.indexnode) + if not node['notypesetting']: + ret.append(node) + else: # replace the node with a target node containing all the ids of # this node and its children. - node = nodes.target(ids=self.__class__.collect_ids(node)) - self.set_source_info(node) - - return [self.indexnode, node] + # It might happen that there are no ids (e.g. with noindex). + # In this case creating a target without an id breaks docutils + # assumption about targets. Thus, we skip that in this case. + ids = self.__class__.collect_ids(node) + if ids: + targetnode = nodes.target(ids=ids) + self.set_source_info(targetnode) + ret.append(targetnode) + return ret @staticmethod def collect_ids(node: nodes.Node) -> List[str]: From d975c60880dfc2afd2ab8a8a538bef9059453475 Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 16/31] ObjectDescription: Add tests for :notypesetting: option --- tests/test_directives.py | 108 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 tests/test_directives.py diff --git a/tests/test_directives.py b/tests/test_directives.py new file mode 100644 index 00000000000..ff88f21d980 --- /dev/null +++ b/tests/test_directives.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_notypesetting(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): + text = (f".. {directive}:: {sig_f}\n" + f" :notypesetting:\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_notypesetting_twice(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): + text = (f".. {directive}:: {sig_f}\n" + f" :notypesetting:\n" + f".. {directive}:: {sig_g}\n" + f" :notypesetting:\n") + doctree = restructuredtext.parse(app, text) + # Note that all index come before the targets + 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_notypesetting_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_notypesetting_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" :notypesetting:\n" + f".. {directive}:: {sig_g}\n" + f" :notypesetting:\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_notypesetting_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" :notypesetting:\n" + f".. {directive}:: {sig_g}\n" + f" :notypesetting:\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_notypesetting_code(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): + text = (f".. {directive}:: {sig_f}\n" + f" :notypesetting:\n" + f".. {directive}:: {sig_g}\n" + f" :notypesetting:\n" + f".. code::\n" + f"\n" + f" code\n") + doctree = restructuredtext.parse(app, text) + # Note that all index 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_notypesetting_heading(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): + text = (f".. {directive}:: {sig_f}\n" + f" :notypesetting:\n" + f".. {directive}:: {sig_g}\n" + f" :notypesetting:\n" + f"\n" + f"Heading\n" + f"=======\n") + doctree = restructuredtext.parse(app, text) + # Note that all index come before the targets and the heading is floated before those. + assert_node(doctree, (nodes.title, addnodes.index, addnodes.index, nodes.target, nodes.target)) From 3e791c9e1fb0a9df75a83a3f151772ea2b72c350 Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 17/31] Reorder consecutive index and target nodes An index directive creates a index node followed by a target node. Two consecutive index directives:: .. index:: first .. index:: second create index, target, index, target, i.e. a mixture. The interspersed index nodes prevent other transformations, e.g. PropagateTargets to properly work on the target nodes. Apply a transformation which reorders mixed and consecutive index and target nodes such that first all index nodes are before all target nodes: Given the following document as input: The transformed result will be: --- sphinx/transforms/__init__.py | 78 ++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index b661870bf49..bdc481b843e 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -3,7 +3,7 @@ import re import unicodedata import warnings -from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Tuple, cast +from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Tuple, Union, cast import docutils from docutils import nodes @@ -417,6 +417,81 @@ 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:: + + + + + + + + + + The transformed result will be:: + + + + + + + + + """ + + # Priority must smaller than the one of PropagateTargets transform, which + # has 260. + default_priority = 250 + + def apply(self, **kwargs: Any) -> None: + for target in self.document.findall(nodes.target): + self.reorder_around(target) + + def reorder_around(self, start_node: nodes.Node) -> None: + # collect all follow up target or index sibling nodes (including node + # itself). Note that we cannot use the 'condition' to filter for index + # and target as we want *consecutive* target/index nodes. + nodes_to_reorder: List[Union[nodes.target, addnodes.index]] = [] + node: nodes.Node + for node in start_node.findall(condition=None, + include_self=True, + descend=False, + siblings=True): + if not isinstance(node, nodes.target) and \ + not isinstance(node, addnodes.index): + break # consecutive strike is broken + nodes_to_reorder.append(node) + + if len(nodes_to_reorder) < 2: + return # Nothing to reorder + + # Since we have at least two siblings, their parent is not None and + # supports children (e.g. is not Text) + + parent_node: nodes.Element = nodes_to_reorder[0].parent + assert parent_node == nodes_to_reorder[-1].parent + first_idx = parent_node.index(nodes_to_reorder[0]) + last_idx = parent_node.index(nodes_to_reorder[-1]) + assert first_idx + len(nodes_to_reorder) - 1 == last_idx + + def sortkey(node: nodes.Node) -> int: + if isinstance(node, addnodes.index): + return 1 + elif isinstance(node, nodes.target): + return 2 + else: + raise Exception('This cannot happen! (Unreachable code reached)') + # Important: The sort algorithm used must be a stable sort. + nodes_to_reorder.sort(key = sortkey) + + # '+1' since slices are excluding the right hand index + parent_node[first_idx:last_idx + 1] = nodes_to_reorder + + def setup(app: "Sphinx") -> Dict[str, Any]: app.add_transform(ApplySourceWorkaround) app.add_transform(ExtraTranslatableNodes) @@ -433,6 +508,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', From 0b6ed33a7deb6bddfc2a39a858c7826e7595311a Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 18/31] Domain Python: Fix tests due to new index/target node order The post transform ReorderConsecutiveTargetAndIndexNodes reorders index and target nodes. A snipped like:: .. py:module:: mymodule .. py:data:: myvar generates the nodes:: which get reordered to:: --- tests/test_domain_py.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 02789069c4e..66950cd6699 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -616,8 +616,8 @@ def test_pydata(app): domain = app.env.get_domain('py') doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, - nodes.target, addnodes.index, + nodes.target, [desc, ([desc_signature, ([desc_addname, "example."], [desc_name, "var"], [desc_annotation, ([desc_sig_punctuation, ':'], @@ -641,8 +641,8 @@ def test_pyfunction(app): [desc_parameterlist, ()])], [desc_content, ()])], addnodes.index, - nodes.target, addnodes.index, + nodes.target, [desc, ([desc_signature, ([desc_annotation, ([desc_sig_keyword, 'async'], desc_sig_space)], [desc_addname, "example."], @@ -653,7 +653,7 @@ def test_pyfunction(app): entries=[('pair', 'built-in function; func1()', 'func1', '', None)]) assert_node(doctree[2], addnodes.index, entries=[('pair', 'module; example', 'module-example', '', None)]) - assert_node(doctree[4], addnodes.index, + assert_node(doctree[3], addnodes.index, entries=[('single', 'func2() (in module example)', 'example.func2', '', None)]) assert 'func1' in domain.objects @@ -1011,8 +1011,8 @@ def test_info_field_list(app): print(doctree) assert_node(doctree, (addnodes.index, - nodes.target, addnodes.index, + nodes.target, [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], [desc_addname, "example."], [desc_name, "Class"])], @@ -1102,8 +1102,8 @@ def test_info_field_list_piped_type(app): assert_node(doctree, (addnodes.index, - nodes.target, addnodes.index, + nodes.target, [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], [desc_addname, "example."], [desc_name, "Class"])], @@ -1136,8 +1136,8 @@ def test_info_field_list_Literal(app): assert_node(doctree, (addnodes.index, - nodes.target, addnodes.index, + nodes.target, [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], [desc_addname, "example."], [desc_name, "Class"])], From d1db8f265edecb5ebfbdf9ff7d2c3803345f8163 Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 19/31] Add tests for ReorderConsecutiveTargetAndIndexNodes --- tests/test_transforms.py | 96 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 tests/test_transforms.py diff --git a/tests/test_transforms.py b/tests/test_transforms.py new file mode 100644 index 00000000000..0cfca9d9d77 --- /dev/null +++ b/tests/test_transforms.py @@ -0,0 +1,96 @@ +"""Tests the transformations""" + +from docutils import nodes + +from sphinx import addnodes +from sphinx.testing import restructuredtext +from sphinx.testing.util import assert_node + + +def test_transforms_ReorderConsecutiveTargetAndIndexNodes_preserve_order(app): + text = (".. index:: abc\n" + ".. index:: def\n" + ".. index:: ghi\n" + ".. index:: jkl\n" + "\n" + "text\n") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + addnodes.index, + addnodes.index, + addnodes.index, + nodes.target, + nodes.target, + nodes.target, + nodes.target, + nodes.paragraph)) + assert_node(doctree[0], addnodes.index, entries=[('single', 'abc', 'index-0', '', None)]) + assert_node(doctree[1], addnodes.index, entries=[('single', 'def', 'index-1', '', None)]) + assert_node(doctree[2], addnodes.index, entries=[('single', 'ghi', 'index-2', '', None)]) + assert_node(doctree[3], addnodes.index, entries=[('single', 'jkl', 'index-3', '', None)]) + assert_node(doctree[4], nodes.target, refid="index-0") + assert_node(doctree[5], nodes.target, refid="index-1") + assert_node(doctree[6], nodes.target, refid="index-2") + assert_node(doctree[7], nodes.target, refid="index-3") + # assert_node(doctree[8], nodes.paragraph) + + +def test_transforms_ReorderConsecutiveTargetAndIndexNodes_no_merge_across_other_nodes(app): + text = (".. index:: abc\n" + ".. index:: def\n" + "\n" + "text\n" + "\n" + ".. index:: ghi\n" + ".. index:: jkl\n" + "\n" + "text\n") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + addnodes.index, + nodes.target, + nodes.target, + nodes.paragraph, + addnodes.index, + addnodes.index, + nodes.target, + nodes.target, + nodes.paragraph)) + assert_node(doctree[0], addnodes.index, entries=[('single', 'abc', 'index-0', '', None)]) + assert_node(doctree[1], addnodes.index, entries=[('single', 'def', 'index-1', '', None)]) + assert_node(doctree[2], nodes.target, refid="index-0") + assert_node(doctree[3], nodes.target, refid="index-1") + # assert_node(doctree[4], nodes.paragraph) + assert_node(doctree[5], addnodes.index, entries=[('single', 'ghi', 'index-2', '', None)]) + assert_node(doctree[6], addnodes.index, entries=[('single', 'jkl', 'index-3', '', None)]) + assert_node(doctree[7], nodes.target, refid="index-2") + assert_node(doctree[8], nodes.target, refid="index-3") + # assert_node(doctree[9], nodes.paragraph) + + +def test_transforms_ReorderConsecutiveTargetAndIndexNodes_merge_with_labels(app): + text = (".. _abc:\n" + ".. index:: def\n" + ".. _ghi:\n" + ".. index:: jkl\n" + ".. _mno:\n" + "\n" + "Heading\n" + "=======\n") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (nodes.title, + addnodes.index, + addnodes.index, + nodes.target, + nodes.target, + nodes.target, + nodes.target, + nodes.target)) + # assert_node(doctree[8], nodes.title) + assert_node(doctree[1], addnodes.index, entries=[('single', 'def', 'index-0', '', None)]) + assert_node(doctree[2], addnodes.index, entries=[('single', 'jkl', 'index-1', '', None)]) + assert_node(doctree[3], nodes.target, refid="abc") + assert_node(doctree[4], nodes.target, refid="index-0") + assert_node(doctree[5], nodes.target, refid="ghi") + assert_node(doctree[6], nodes.target, refid="index-1") + assert_node(doctree[7], nodes.target, refid="mno") From fb0ad6c5c6afa6ba7a0a43ccc5d04c3db4995ae9 Mon Sep 17 00:00:00 2001 From: Latosha Maltba <79100569+latosha-maltba@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:23:51 +0000 Subject: [PATCH 20/31] CHANGES: Add entry for ``:notypesetting:`` --- CHANGES | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index b245c78f629..fc6cc295517 100644 --- a/CHANGES +++ b/CHANGES @@ -34,6 +34,10 @@ Deprecated Features added -------------- +* Most domain directives, e.g. ``.. py:function::``, now support the option + ``:notypesetting:`` to suppress their output and only create a linkable + anchor (#9662, #9671, #9675, #10478) + Bugs fixed ---------- From 6f73587dbee24dc1778f460be352c292e2ccd24a Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 28 Jul 2023 18:16:14 +0100 Subject: [PATCH 21/31] typing --- sphinx/directives/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index e99e3d4b7ee..d7e2ebb52f9 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -273,7 +273,7 @@ def run(self) -> list[Node]: self.env.temp_data['object'] = None self.after_content() - ret: List[nodes.Node] = [] + ret: list[nodes.Node] = [] ret.append(self.indexnode) if not node['notypesetting']: ret.append(node) @@ -291,9 +291,9 @@ def run(self) -> list[Node]: return ret @staticmethod - def collect_ids(node: nodes.Node) -> List[str]: + def collect_ids(node: nodes.Node) -> list[str]: if isinstance(node, nodes.Element): - ids: List[str] = node.get('ids', []) + ids: list[str] = node.get('ids', []) for c in node.children: ids.extend(ObjectDescription.collect_ids(c)) return ids From 2857f268e135d4b538346eb357da598c6ab75ccc Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 28 Jul 2023 18:19:22 +0100 Subject: [PATCH 22/31] style --- sphinx/transforms/__init__.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index b3f45aaad09..4d520ed9132 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -454,12 +454,8 @@ def reorder_around(self, start_node: nodes.Node) -> None: # and target as we want *consecutive* target/index nodes. nodes_to_reorder: list[nodes.target | addnodes.index] = [] node: nodes.Node - for node in start_node.findall(condition=None, - include_self=True, - descend=False, - siblings=True): - if not isinstance(node, nodes.target) and \ - not isinstance(node, addnodes.index): + for node in start_node.findall(descend=False, siblings=True): + if not isinstance(node, (nodes.target, addnodes.index)): break # consecutive strike is broken nodes_to_reorder.append(node) From 75977dd8294845567feca2a06956ae9bdb1376f6 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 28 Jul 2023 18:20:26 +0100 Subject: [PATCH 23/31] notypesetting -> no-typesetting --- CHANGES | 2 +- doc/usage/restructuredtext/domains.rst | 8 +++--- sphinx/directives/__init__.py | 6 ++--- sphinx/domains/c.py | 2 +- sphinx/domains/cpp.py | 2 +- tests/test_directives.py | 36 +++++++++++++------------- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/CHANGES b/CHANGES index 47eebd053a4..387b1494f7f 100644 --- a/CHANGES +++ b/CHANGES @@ -43,7 +43,7 @@ Release 7.1.1 (released Jul 27, 2023) ===================================== * Most domain directives, e.g. ``.. py:function::``, now support the option - ``:notypesetting:`` to suppress their output and only create a linkable + ``:no-typesetting:`` to suppress their output and only create a linkable anchor (#9662, #9671, #9675, #10478) Bugs fixed diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index bf1652bdf5e..5c5e311979f 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -55,7 +55,7 @@ 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 -``:notypesetting:``. This can for example be used to create only a target and +``: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. @@ -69,7 +69,7 @@ options. and reStructuredText domains. .. versionadded:: 7.2 - The directive option ``notypesetting``. + The directive option ``no-typesetting``. An example using a Python domain directive:: @@ -97,12 +97,12 @@ you could say :: As you can see, both directive and role names contain the domain name and the directive name. -The directive option ``:notypesetting:`` can be used to create a target (and +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:: .. py:function:: spam(eggs) - :notypesetting: + :no-typesetting: .. code:: def spam(eggs): diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index d7e2ebb52f9..7af52e51b39 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -57,7 +57,7 @@ class ObjectDescription(SphinxDirective, Generic[ObjDescT]): 'noindex': directives.flag, 'noindexentry': directives.flag, 'nocontentsentry': directives.flag, - 'notypesetting': directives.flag, + 'no-typesetting': directives.flag, } # types of doc fields that this directive handles, see sphinx.util.docfields @@ -219,7 +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['notypesetting'] = 'notypesetting' in self.options + node['no-typesetting'] = 'no-typesetting' in self.options if self.domain: node['classes'].append(self.domain) node['classes'].append(node['objtype']) @@ -275,7 +275,7 @@ def run(self) -> list[Node]: ret: list[nodes.Node] = [] ret.append(self.indexnode) - if not node['notypesetting']: + if not node['no-typesetting']: ret.append(node) else: # replace the node with a target node containing all the ids of diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index a87ca5cb674..2e85c1bc59e 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -3629,7 +3629,7 @@ class CAliasObject(ObjectDescription): 'noroot': directives.flag, }) del option_spec['noindex'] # is in ObjectDescription but doesn't make sense here - del option_spec['notypesetting'] # is in ObjectDescription but doesn't make sense here + del option_spec['no-typesetting'] # is in ObjectDescription but doesn't make sense here def run(self) -> list[Node]: """ diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 6b7b8584b79..807b748e446 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -7747,7 +7747,7 @@ class CPPAliasObject(ObjectDescription): 'noroot': directives.flag, }) del option_spec['noindex'] # is in ObjectDescription but doesn't make sense here - del option_spec['notypesetting'] # is in ObjectDescription but doesn't make sense here + del option_spec['no-typesetting'] # is in ObjectDescription but doesn't make sense here def run(self) -> list[Node]: """ diff --git a/tests/test_directives.py b/tests/test_directives.py index ff88f21d980..5473156a331 100644 --- a/tests/test_directives.py +++ b/tests/test_directives.py @@ -20,26 +20,26 @@ @pytest.mark.parametrize("directive,noindex,noindexentry,sig_f,sig_g,index_g", DOMAINS) -def test_object_description_notypesetting(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): +def test_object_description_no-typesetting(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): text = (f".. {directive}:: {sig_f}\n" - f" :notypesetting:\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_notypesetting_twice(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): +def test_object_description_no-typesetting_twice(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): text = (f".. {directive}:: {sig_f}\n" - f" :notypesetting:\n" + f" :no-typesetting:\n" f".. {directive}:: {sig_g}\n" - f" :notypesetting:\n") + f" :no-typesetting:\n") doctree = restructuredtext.parse(app, text) # Note that all index come before the targets 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_notypesetting_noindex_orig(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): +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" @@ -51,14 +51,14 @@ def test_object_description_notypesetting_noindex_orig(app, directive, noindex, @pytest.mark.parametrize("directive,noindex,noindexentry,sig_f,sig_g,index_g", DOMAINS) -def test_object_description_notypesetting_noindex(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): +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" :notypesetting:\n" + f" :no-typesetting:\n" f".. {directive}:: {sig_g}\n" - f" :notypesetting:\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=[]) @@ -66,14 +66,14 @@ def test_object_description_notypesetting_noindex(app, directive, noindex, noind @pytest.mark.parametrize("directive,noindex,noindexentry,sig_f,sig_g,index_g", DOMAINS) -def test_object_description_notypesetting_noindexentry(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): +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" :notypesetting:\n" + f" :no-typesetting:\n" f".. {directive}:: {sig_g}\n" - f" :notypesetting:\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=[]) @@ -81,11 +81,11 @@ def test_object_description_notypesetting_noindexentry(app, directive, noindex, @pytest.mark.parametrize("directive,noindex,noindexentry,sig_f,sig_g,index_g", DOMAINS) -def test_object_description_notypesetting_code(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): +def test_object_description_no-typesetting_code(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): text = (f".. {directive}:: {sig_f}\n" - f" :notypesetting:\n" + f" :no-typesetting:\n" f".. {directive}:: {sig_g}\n" - f" :notypesetting:\n" + f" :no-typesetting:\n" f".. code::\n" f"\n" f" code\n") @@ -95,11 +95,11 @@ def test_object_description_notypesetting_code(app, directive, noindex, noindexe @pytest.mark.parametrize("directive,noindex,noindexentry,sig_f,sig_g,index_g", DOMAINS) -def test_object_description_notypesetting_heading(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): +def test_object_description_no-typesetting_heading(app, directive, noindex, noindexentry, sig_f, sig_g, index_g): text = (f".. {directive}:: {sig_f}\n" - f" :notypesetting:\n" + f" :no-typesetting:\n" f".. {directive}:: {sig_g}\n" - f" :notypesetting:\n" + f" :no-typesetting:\n" f"\n" f"Heading\n" f"=======\n") From a5a708a75521bb06c55311ca3f046ee4adac58e3 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 28 Jul 2023 18:25:48 +0100 Subject: [PATCH 24/31] Don't copy ``ObjectDescription.option_spec`` --- sphinx/domains/c.py | 14 +++++--------- sphinx/domains/cpp.py | 14 +++++--------- sphinx/domains/javascript.py | 7 ++++--- sphinx/domains/python.py | 7 ++++--- 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index 2e85c1bc59e..af0ca6230c3 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -3163,13 +3163,12 @@ class CObject(ObjectDescription[ASTDeclaration]): Description of a C language object. """ - option_spec: OptionSpec = ObjectDescription.option_spec.copy() - option_spec.update({ + option_spec: OptionSpec = { 'noindexentry': directives.flag, 'nocontentsentry': directives.flag, + 'no-typesetting': directives.flag, 'single-line-parameter-list': directives.flag, - }) - del option_spec['noindex'] # is in ObjectDescription but doesn't make sense here + } def _add_enumerator_to_parent(self, ast: ASTDeclaration) -> None: assert ast.objectType == 'enumerator' @@ -3623,13 +3622,10 @@ def apply(self, **kwargs: Any) -> None: class CAliasObject(ObjectDescription): - option_spec: OptionSpec = ObjectDescription.option_spec.copy() - option_spec.update({ + option_spec: OptionSpec = { 'maxdepth': directives.nonnegative_int, 'noroot': directives.flag, - }) - del option_spec['noindex'] # is in ObjectDescription but doesn't make sense here - del option_spec['no-typesetting'] # is in ObjectDescription but doesn't make sense here + } def run(self) -> list[Node]: """ diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 807b748e446..d462f7d4b46 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -7211,14 +7211,13 @@ class CPPObject(ObjectDescription[ASTDeclaration]): can_collapse=True), ] - option_spec: OptionSpec = ObjectDescription.option_spec.copy() - option_spec.update({ + option_spec: OptionSpec = { 'noindexentry': directives.flag, 'nocontentsentry': directives.flag, + 'no-typesetting': directives.flag, 'tparam-line-spec': directives.flag, 'single-line-parameter-list': directives.flag, - }) - del option_spec['noindex'] # is in ObjectDescription but doesn't make sense here + } def _add_enumerator_to_parent(self, ast: ASTDeclaration) -> None: assert ast.objectType == 'enumerator' @@ -7741,13 +7740,10 @@ def apply(self, **kwargs: Any) -> None: class CPPAliasObject(ObjectDescription): - option_spec: OptionSpec = ObjectDescription.option_spec.copy() - option_spec.update({ + option_spec: OptionSpec = { 'maxdepth': directives.nonnegative_int, 'noroot': directives.flag, - }) - del option_spec['noindex'] # is in ObjectDescription but doesn't make sense here - del option_spec['no-typesetting'] # is in ObjectDescription but doesn't make sense here + } def run(self) -> list[Node]: """ diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index b05a597f28c..67fca719765 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -42,12 +42,13 @@ class JSObject(ObjectDescription[tuple[str, str]]): #: based on directive nesting allow_nesting = False - option_spec: OptionSpec = ObjectDescription.option_spec.copy() - option_spec.update({ + option_spec: OptionSpec = { + 'noindex': directives.flag, 'noindexentry': directives.flag, 'nocontentsentry': directives.flag, + 'no-typesetting': directives.flag, 'single-line-parameter-list': directives.flag, - }) + } def get_display_prefix(self) -> list[Node]: #: what is displayed right before the documentation entry diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 7b68d67904f..8db336d0f4b 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -658,16 +658,17 @@ class PyObject(ObjectDescription[tuple[str, str]]): :cvar allow_nesting: Class is an object that allows for nested namespaces :vartype allow_nesting: bool """ - option_spec: OptionSpec = ObjectDescription.option_spec.copy() - option_spec.update({ + option_spec: OptionSpec = { + '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, 'canonical': directives.unchanged, 'annotation': directives.unchanged, - }) + } doc_field_types = [ PyTypedField('parameter', label=_('Parameters'), From 5050ec5c6f5709a1d97e2477875cac7c5bb975cb Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 28 Jul 2023 18:27:14 +0100 Subject: [PATCH 25/31] Add to reST domain --- doc/usage/restructuredtext/domains.rst | 3 ++- sphinx/domains/rst.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index 5c5e311979f..ac43881ffc1 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -69,7 +69,8 @@ options. and reStructuredText domains. .. versionadded:: 7.2 - The directive option ``no-typesetting``. + The directive option ``no-typesetting`` in the Python, C, C++, Javascript, + and reStructuredText domains. An example using a Python domain directive:: diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py index 2822aa7ff1a..e850653ee20 100644 --- a/sphinx/domains/rst.py +++ b/sphinx/domains/rst.py @@ -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: From 6f45edb965dddb990a0469fdca3e671a150e2b16 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 28 Jul 2023 18:28:09 +0100 Subject: [PATCH 26/31] Add to JSModule and PythonModule --- sphinx/domains/javascript.py | 1 + sphinx/domains/python.py | 1 + 2 files changed, 2 insertions(+) diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index 67fca719765..94e568f90ca 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -294,6 +294,7 @@ class JSModule(SphinxDirective): option_spec: OptionSpec = { 'noindex': directives.flag, 'nocontentsentry': directives.flag, + 'no-typesetting': directives.flag, } def run(self) -> list[Node]: diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 8db336d0f4b..416bee1cdf6 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -1263,6 +1263,7 @@ class PyModule(SphinxDirective): 'synopsis': lambda x: x, 'noindex': directives.flag, 'nocontentsentry': directives.flag, + 'no-typesetting': directives.flag, 'deprecated': directives.flag, } From 184773ce4207c846137a6166f3f575cdc6c24e34 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 28 Jul 2023 19:29:27 +0100 Subject: [PATCH 27/31] Updates --- doc/usage/restructuredtext/domains.rst | 10 ++-- sphinx/directives/__init__.py | 41 +++++---------- sphinx/transforms/__init__.py | 72 +++++++++++++------------- tests/test_directives.py | 34 ++++++------ tests/test_transforms.py | 6 +-- 5 files changed, 77 insertions(+), 86 deletions(-) diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index ac43881ffc1..da53f7efbd8 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -98,12 +98,16 @@ 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:: +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): diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 7af52e51b39..2f10649bb09 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -219,7 +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 + node['no-typesetting'] = ('no-typesetting' in self.options) if self.domain: node['classes'].append(self.domain) node['classes'].append(node['objtype']) @@ -273,32 +273,19 @@ def run(self) -> list[Node]: self.env.temp_data['object'] = None self.after_content() - ret: list[nodes.Node] = [] - ret.append(self.indexnode) - if not node['no-typesetting']: - ret.append(node) - else: - # replace the node with a target node containing all the ids of - # this node and its children. - # It might happen that there are no ids (e.g. with noindex). - # In this case creating a target without an id breaks docutils - # assumption about targets. Thus, we skip that in this case. - ids = self.__class__.collect_ids(node) - if ids: - targetnode = nodes.target(ids=ids) - self.set_source_info(targetnode) - ret.append(targetnode) - return ret - - @staticmethod - def collect_ids(node: nodes.Node) -> list[str]: - if isinstance(node, nodes.Element): - ids: list[str] = node.get('ids', []) - for c in node.children: - ids.extend(ObjectDescription.collect_ids(c)) - return ids - else: - return [] + 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] class DefaultRole(SphinxDirective): diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index 4d520ed9132..3d6c447e36e 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -440,49 +440,49 @@ class ReorderConsecutiveTargetAndIndexNodes(SphinxTransform): """ - # Priority must smaller than the one of PropagateTargets transform, which - # has 260. - default_priority = 250 + # This transform MUST run before ``PropagateTargets``. + default_priority = 220 def apply(self, **kwargs: Any) -> None: for target in self.document.findall(nodes.target): - self.reorder_around(target) - - def reorder_around(self, start_node: nodes.Node) -> None: - # collect all follow up target or index sibling nodes (including node - # itself). Note that we cannot use the 'condition' to filter for index - # and target as we want *consecutive* target/index nodes. - nodes_to_reorder: list[nodes.target | addnodes.index] = [] - node: nodes.Node - for node in start_node.findall(descend=False, siblings=True): - if not isinstance(node, (nodes.target, addnodes.index)): - break # consecutive strike is broken - nodes_to_reorder.append(node) - - if len(nodes_to_reorder) < 2: - return # Nothing to reorder + _reorder_index_target_nodes(target) - # Since we have at least two siblings, their parent is not None and - # supports children (e.g. is not Text) - parent_node: nodes.Element = nodes_to_reorder[0].parent - assert parent_node == nodes_to_reorder[-1].parent - first_idx = parent_node.index(nodes_to_reorder[0]) - last_idx = parent_node.index(nodes_to_reorder[-1]) - assert first_idx + len(nodes_to_reorder) - 1 == last_idx +def _reorder_index_target_nodes(start_node: nodes.target) -> None: + """Sort target and index nodes. - def sortkey(node: nodes.Node) -> int: - if isinstance(node, addnodes.index): - return 1 - elif isinstance(node, nodes.target): - return 2 - else: - raise Exception('This cannot happen! (Unreachable code reached)') - # Important: The sort algorithm used must be a stable sort. - nodes_to_reorder.sort(key = sortkey) + 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] = [] - # '+1' since slices are excluding the right hand index - parent_node[first_idx:last_idx + 1] = nodes_to_reorder + # 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]: diff --git a/tests/test_directives.py b/tests/test_directives.py index 5473156a331..d7292bc1277 100644 --- a/tests/test_directives.py +++ b/tests/test_directives.py @@ -19,27 +19,27 @@ ] -@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): +@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): +@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 come before the targets + # 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): +@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" @@ -50,8 +50,8 @@ def test_object_description_no-typesetting_noindex_orig(app, directive, noindex, 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): +@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" @@ -65,8 +65,8 @@ def test_object_description_no-typesetting_noindex(app, directive, noindex, noin 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): +@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" @@ -80,8 +80,8 @@ def test_object_description_no-typesetting_noindexentry(app, directive, noindex, 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): +@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" @@ -90,12 +90,12 @@ def test_object_description_no-typesetting_code(app, directive, noindex, noindex f"\n" f" code\n") doctree = restructuredtext.parse(app, text) - # Note that all index come before the targets + # 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): +@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" @@ -104,5 +104,5 @@ def test_object_description_no-typesetting_heading(app, directive, noindex, noin f"Heading\n" f"=======\n") doctree = restructuredtext.parse(app, text) - # Note that all index come before the targets and the heading is floated before those. + # 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)) diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 0cfca9d9d77..170d1811950 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -7,7 +7,7 @@ from sphinx.testing.util import assert_node -def test_transforms_ReorderConsecutiveTargetAndIndexNodes_preserve_order(app): +def test_transforms_reorder_consecutive_target_and_index_nodes_preserve_order(app): text = (".. index:: abc\n" ".. index:: def\n" ".. index:: ghi\n" @@ -35,7 +35,7 @@ def test_transforms_ReorderConsecutiveTargetAndIndexNodes_preserve_order(app): # assert_node(doctree[8], nodes.paragraph) -def test_transforms_ReorderConsecutiveTargetAndIndexNodes_no_merge_across_other_nodes(app): +def test_transforms_reorder_consecutive_target_and_index_nodes_no_merge_across_other_nodes(app): text = (".. index:: abc\n" ".. index:: def\n" "\n" @@ -68,7 +68,7 @@ def test_transforms_ReorderConsecutiveTargetAndIndexNodes_no_merge_across_other_ # assert_node(doctree[9], nodes.paragraph) -def test_transforms_ReorderConsecutiveTargetAndIndexNodes_merge_with_labels(app): +def test_transforms_reorder_consecutive_target_and_index_nodes_merge_with_labels(app): text = (".. _abc:\n" ".. index:: def\n" ".. _ghi:\n" From 321816b06e82793342a06cb86e3d6dcf531645ca Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 28 Jul 2023 19:32:13 +0100 Subject: [PATCH 28/31] Quotation marks --- tests/test_directives.py | 72 ++++++++++++++++++++-------------------- tests/test_transforms.py | 72 ++++++++++++++++++++-------------------- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/tests/test_directives.py b/tests/test_directives.py index d7292bc1277..f22e2497f6c 100644 --- a/tests/test_directives.py +++ b/tests/test_directives.py @@ -21,18 +21,18 @@ @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") + 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") + 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)) @@ -41,10 +41,10 @@ def test_object_description_no_typesetting_twice(app, directive, noindex, noinde @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") + 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]) @@ -53,12 +53,12 @@ def test_object_description_no_typesetting_noindex_orig(app, directive, noindex, @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") + 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=[]) @@ -68,12 +68,12 @@ def test_object_description_no_typesetting_noindex(app, directive, noindex, noin @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") + 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=[]) @@ -82,13 +82,13 @@ def test_object_description_no_typesetting_noindexentry(app, directive, noindex, @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") + 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)) @@ -96,13 +96,13 @@ def test_object_description_no_typesetting_code(app, directive, noindex, noindex @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") + 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)) diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 170d1811950..7ffdae6c613 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -8,12 +8,12 @@ def test_transforms_reorder_consecutive_target_and_index_nodes_preserve_order(app): - text = (".. index:: abc\n" - ".. index:: def\n" - ".. index:: ghi\n" - ".. index:: jkl\n" - "\n" - "text\n") + text = ('.. index:: abc\n' + '.. index:: def\n' + '.. index:: ghi\n' + '.. index:: jkl\n' + '\n' + 'text\n') doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, addnodes.index, @@ -28,23 +28,23 @@ def test_transforms_reorder_consecutive_target_and_index_nodes_preserve_order(ap assert_node(doctree[1], addnodes.index, entries=[('single', 'def', 'index-1', '', None)]) assert_node(doctree[2], addnodes.index, entries=[('single', 'ghi', 'index-2', '', None)]) assert_node(doctree[3], addnodes.index, entries=[('single', 'jkl', 'index-3', '', None)]) - assert_node(doctree[4], nodes.target, refid="index-0") - assert_node(doctree[5], nodes.target, refid="index-1") - assert_node(doctree[6], nodes.target, refid="index-2") - assert_node(doctree[7], nodes.target, refid="index-3") + assert_node(doctree[4], nodes.target, refid='index-0') + assert_node(doctree[5], nodes.target, refid='index-1') + assert_node(doctree[6], nodes.target, refid='index-2') + assert_node(doctree[7], nodes.target, refid='index-3') # assert_node(doctree[8], nodes.paragraph) def test_transforms_reorder_consecutive_target_and_index_nodes_no_merge_across_other_nodes(app): - text = (".. index:: abc\n" - ".. index:: def\n" - "\n" - "text\n" - "\n" - ".. index:: ghi\n" - ".. index:: jkl\n" - "\n" - "text\n") + text = ('.. index:: abc\n' + '.. index:: def\n' + '\n' + 'text\n' + '\n' + '.. index:: ghi\n' + '.. index:: jkl\n' + '\n' + 'text\n') doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, addnodes.index, @@ -58,25 +58,25 @@ def test_transforms_reorder_consecutive_target_and_index_nodes_no_merge_across_o nodes.paragraph)) assert_node(doctree[0], addnodes.index, entries=[('single', 'abc', 'index-0', '', None)]) assert_node(doctree[1], addnodes.index, entries=[('single', 'def', 'index-1', '', None)]) - assert_node(doctree[2], nodes.target, refid="index-0") - assert_node(doctree[3], nodes.target, refid="index-1") + assert_node(doctree[2], nodes.target, refid='index-0') + assert_node(doctree[3], nodes.target, refid='index-1') # assert_node(doctree[4], nodes.paragraph) assert_node(doctree[5], addnodes.index, entries=[('single', 'ghi', 'index-2', '', None)]) assert_node(doctree[6], addnodes.index, entries=[('single', 'jkl', 'index-3', '', None)]) - assert_node(doctree[7], nodes.target, refid="index-2") - assert_node(doctree[8], nodes.target, refid="index-3") + assert_node(doctree[7], nodes.target, refid='index-2') + assert_node(doctree[8], nodes.target, refid='index-3') # assert_node(doctree[9], nodes.paragraph) def test_transforms_reorder_consecutive_target_and_index_nodes_merge_with_labels(app): - text = (".. _abc:\n" - ".. index:: def\n" - ".. _ghi:\n" - ".. index:: jkl\n" - ".. _mno:\n" - "\n" - "Heading\n" - "=======\n") + text = ('.. _abc:\n' + '.. index:: def\n' + '.. _ghi:\n' + '.. index:: jkl\n' + '.. _mno:\n' + '\n' + 'Heading\n' + '=======\n') doctree = restructuredtext.parse(app, text) assert_node(doctree, (nodes.title, addnodes.index, @@ -89,8 +89,8 @@ def test_transforms_reorder_consecutive_target_and_index_nodes_merge_with_labels # assert_node(doctree[8], nodes.title) assert_node(doctree[1], addnodes.index, entries=[('single', 'def', 'index-0', '', None)]) assert_node(doctree[2], addnodes.index, entries=[('single', 'jkl', 'index-1', '', None)]) - assert_node(doctree[3], nodes.target, refid="abc") - assert_node(doctree[4], nodes.target, refid="index-0") - assert_node(doctree[5], nodes.target, refid="ghi") - assert_node(doctree[6], nodes.target, refid="index-1") - assert_node(doctree[7], nodes.target, refid="mno") + assert_node(doctree[3], nodes.target, refid='abc') + assert_node(doctree[4], nodes.target, refid='index-0') + assert_node(doctree[5], nodes.target, refid='ghi') + assert_node(doctree[6], nodes.target, refid='index-1') + assert_node(doctree[7], nodes.target, refid='mno') From 40484b5d0644b39dd40ce9381c0ef6564420977f Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 28 Jul 2023 19:33:08 +0100 Subject: [PATCH 29/31] rename --- tests/{test_transforms.py => test_transforms_reorder_nodes.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_transforms.py => test_transforms_reorder_nodes.py} (100%) diff --git a/tests/test_transforms.py b/tests/test_transforms_reorder_nodes.py similarity index 100% rename from tests/test_transforms.py rename to tests/test_transforms_reorder_nodes.py From 3c4606448c3610bfc9236c0f7c23a3c0b52b791e Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 28 Jul 2023 19:33:42 +0100 Subject: [PATCH 30/31] rename --- tests/{test_directives.py => test_directives_no_typesetting.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_directives.py => test_directives_no_typesetting.py} (100%) diff --git a/tests/test_directives.py b/tests/test_directives_no_typesetting.py similarity index 100% rename from tests/test_directives.py rename to tests/test_directives_no_typesetting.py From 1db991f709cf54056a88189edfb50646d5950ee6 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 28 Jul 2023 19:37:19 +0100 Subject: [PATCH 31/31] Update CHANGES entry. --- CHANGES | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 387b1494f7f..4da76996d03 100644 --- a/CHANGES +++ b/CHANGES @@ -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 ---------- @@ -42,10 +45,6 @@ Testing Release 7.1.1 (released Jul 27, 2023) ===================================== -* Most domain directives, e.g. ``.. py:function::``, now support the option - ``:no-typesetting:`` to suppress their output and only create a linkable - anchor (#9662, #9671, #9675, #10478) - Bugs fixed ----------