Skip to content

Commit 19018f0

Browse files
picnixzAA-Turner
andauthored
Improve SigElementFallbackTransform fallback logic. (#11311)
Co-authored-by: Adam Turner <[email protected]>
1 parent 65fd5be commit 19018f0

File tree

6 files changed

+346
-31
lines changed

6 files changed

+346
-31
lines changed

CHANGES

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ Features added
5353
Patch by Halldor Fannar.
5454
* #11570: Use short names when using :pep:`585` built-in generics.
5555
Patch by Riccardo Mori.
56+
* #11300: Improve ``SigElementFallbackTransform`` fallback logic and signature
57+
text elements nodes. See :doc:`the documentation </extdev/nodes>` for more
58+
details.
59+
Patch by Bénédikt Tran.
5660

5761
Bugs fixed
5862
----------

doc/extdev/nodes.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,39 @@ and in :py:class:`desc_signature_line` nodes.
3535
.. autoclass:: desc_optional
3636
.. autoclass:: desc_annotation
3737

38+
Nodes for signature text elements
39+
.................................
40+
41+
These nodes inherit :py:class:`desc_sig_element` and are generally translated
42+
to ``docutils.nodes.inline`` by :py:class:`!SigElementFallbackTransform`.
43+
44+
Extensions may create additional ``desc_sig_*``-like nodes but in order for
45+
:py:class:`!SigElementFallbackTransform` to translate them to inline nodes
46+
automatically, they must be added to :py:data:`SIG_ELEMENTS` via the class
47+
keyword argument `_sig_element=True` of :py:class:`desc_sig_element`, e.g.:
48+
49+
.. code-block:: python
50+
51+
class desc_custom_sig_node(desc_sig_element, _sig_element=True): ...
52+
53+
For backwards compatibility, it is still possible to add the nodes directly
54+
using ``SIG_ELEMENTS.add(desc_custom_sig_node)``.
55+
56+
.. autodata:: SIG_ELEMENTS
57+
:no-value:
58+
59+
.. autoclass:: desc_sig_element
60+
61+
.. autoclass:: desc_sig_space
62+
.. autoclass:: desc_sig_name
63+
.. autoclass:: desc_sig_operator
64+
.. autoclass:: desc_sig_punctuation
65+
.. autoclass:: desc_sig_keyword
66+
.. autoclass:: desc_sig_keyword_type
67+
.. autoclass:: desc_sig_literal_number
68+
.. autoclass:: desc_sig_literal_string
69+
.. autoclass:: desc_sig_literal_char
70+
3871
New admonition-like constructs
3972
------------------------------
4073

sphinx/addnodes.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -298,9 +298,20 @@ class desc_annotation(nodes.Part, nodes.Inline, nodes.FixedTextElement):
298298
# Leaf nodes for markup of text fragments
299299
#########################################
300300

301+
#: A set of classes inheriting :class:`desc_sig_element`. Each node class
302+
#: is expected to be handled by the builder's translator class if the latter
303+
#: does not inherit from SphinxTranslator.
304+
#:
305+
#: This set can be extended manually by third-party extensions or
306+
#: by subclassing :class:`desc_sig_element` and using the class
307+
#: keyword argument `_sig_element=True`.
308+
SIG_ELEMENTS: set[type[desc_sig_element]] = set()
309+
310+
301311
# Signature text elements, generally translated to node.inline
302312
# in SigElementFallbackTransform.
303-
# When adding a new one, add it to SIG_ELEMENTS.
313+
# When adding a new one, add it to SIG_ELEMENTS via the class
314+
# keyword argument `_sig_element=True` (e.g., see `desc_sig_space`).
304315

305316
class desc_sig_element(nodes.inline, _desc_classes_injector):
306317
"""Common parent class of nodes for inline text of a signature."""
@@ -311,11 +322,17 @@ def __init__(self, rawsource: str = '', text: str = '',
311322
super().__init__(rawsource, text, *children, **attributes)
312323
self['classes'].extend(self.classes)
313324

325+
def __init_subclass__(cls, *, _sig_element=False, **kwargs):
326+
super().__init_subclass__(**kwargs)
327+
if _sig_element:
328+
# add the class to the SIG_ELEMENTS set if asked
329+
SIG_ELEMENTS.add(cls)
330+
314331

315332
# to not reinvent the wheel, the classes in the following desc_sig classes
316333
# are based on those used in Pygments
317334

318-
class desc_sig_space(desc_sig_element):
335+
class desc_sig_space(desc_sig_element, _sig_element=True):
319336
"""Node for a space in a signature."""
320337
classes = ["w"]
321338

@@ -324,54 +341,46 @@ def __init__(self, rawsource: str = '', text: str = ' ',
324341
super().__init__(rawsource, text, *children, **attributes)
325342

326343

327-
class desc_sig_name(desc_sig_element):
344+
class desc_sig_name(desc_sig_element, _sig_element=True):
328345
"""Node for an identifier in a signature."""
329346
classes = ["n"]
330347

331348

332-
class desc_sig_operator(desc_sig_element):
349+
class desc_sig_operator(desc_sig_element, _sig_element=True):
333350
"""Node for an operator in a signature."""
334351
classes = ["o"]
335352

336353

337-
class desc_sig_punctuation(desc_sig_element):
354+
class desc_sig_punctuation(desc_sig_element, _sig_element=True):
338355
"""Node for punctuation in a signature."""
339356
classes = ["p"]
340357

341358

342-
class desc_sig_keyword(desc_sig_element):
359+
class desc_sig_keyword(desc_sig_element, _sig_element=True):
343360
"""Node for a general keyword in a signature."""
344361
classes = ["k"]
345362

346363

347-
class desc_sig_keyword_type(desc_sig_element):
364+
class desc_sig_keyword_type(desc_sig_element, _sig_element=True):
348365
"""Node for a keyword which is a built-in type in a signature."""
349366
classes = ["kt"]
350367

351368

352-
class desc_sig_literal_number(desc_sig_element):
369+
class desc_sig_literal_number(desc_sig_element, _sig_element=True):
353370
"""Node for a numeric literal in a signature."""
354371
classes = ["m"]
355372

356373

357-
class desc_sig_literal_string(desc_sig_element):
374+
class desc_sig_literal_string(desc_sig_element, _sig_element=True):
358375
"""Node for a string literal in a signature."""
359376
classes = ["s"]
360377

361378

362-
class desc_sig_literal_char(desc_sig_element):
379+
class desc_sig_literal_char(desc_sig_element, _sig_element=True):
363380
"""Node for a character literal in a signature."""
364381
classes = ["sc"]
365382

366383

367-
SIG_ELEMENTS = [desc_sig_space,
368-
desc_sig_name,
369-
desc_sig_operator,
370-
desc_sig_punctuation,
371-
desc_sig_keyword, desc_sig_keyword_type,
372-
desc_sig_literal_number, desc_sig_literal_string, desc_sig_literal_char]
373-
374-
375384
###############################################################
376385
# new admonition-like constructs
377386

sphinx/transforms/post_transforms/__init__.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,18 +250,27 @@ def has_visitor(translator: type[nodes.NodeVisitor], node: type[Element]) -> boo
250250
# subclass of SphinxTranslator supports desc_sig_element nodes automatically.
251251
return
252252

253-
# for the leaf elements (desc_sig_element), the translator should support _all_
254-
if not all(has_visitor(translator, node) for node in addnodes.SIG_ELEMENTS):
253+
# for the leaf elements (desc_sig_element), the translator should support _all_,
254+
# unless there exists a generic visit_desc_sig_element default visitor
255+
if (not all(has_visitor(translator, node) for node in addnodes.SIG_ELEMENTS)
256+
and not has_visitor(translator, addnodes.desc_sig_element)):
255257
self.fallback(addnodes.desc_sig_element)
256258

257259
if not has_visitor(translator, addnodes.desc_inline):
258260
self.fallback(addnodes.desc_inline)
259261

260-
def fallback(self, nodeType: Any) -> None:
261-
for node in self.document.findall(nodeType):
262+
def fallback(self, node_type: Any) -> None:
263+
"""Translate nodes of type *node_type* to docutils inline nodes.
264+
265+
The original node type name is stored as a string in a private
266+
``_sig_node_type`` attribute if the latter did not exist.
267+
"""
268+
for node in self.document.findall(node_type):
262269
newnode = nodes.inline()
263270
newnode.update_all_atts(node)
264271
newnode.extend(node)
272+
# Only set _sig_node_type if not defined by the user
273+
newnode.setdefault('_sig_node_type', node.tagname)
265274
node.replace_self(newnode)
266275

267276

tests/test_addnodes.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Test the non-trivial features in the :mod:`sphinx.addnodes` module."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from sphinx import addnodes
8+
9+
10+
@pytest.fixture()
11+
def sig_elements() -> set[type[addnodes.desc_sig_element]]:
12+
"""Fixture returning the current ``addnodes.SIG_ELEMENTS`` set."""
13+
original = addnodes.SIG_ELEMENTS.copy() # safe copy of the current nodes
14+
yield {*addnodes.SIG_ELEMENTS} # temporary value to use during tests
15+
addnodes.SIG_ELEMENTS = original # restore the previous value
16+
17+
18+
def test_desc_sig_element_nodes(sig_elements):
19+
"""Test the registration of ``desc_sig_element`` subclasses."""
20+
21+
# expected desc_sig_* node classes (must be declared *after* reloading
22+
# the module since otherwise the objects are not the correct ones)
23+
EXPECTED_SIG_ELEMENTS = {
24+
addnodes.desc_sig_space,
25+
addnodes.desc_sig_name,
26+
addnodes.desc_sig_operator,
27+
addnodes.desc_sig_punctuation,
28+
addnodes.desc_sig_keyword,
29+
addnodes.desc_sig_keyword_type,
30+
addnodes.desc_sig_literal_number,
31+
addnodes.desc_sig_literal_string,
32+
addnodes.desc_sig_literal_char,
33+
}
34+
35+
assert addnodes.SIG_ELEMENTS == EXPECTED_SIG_ELEMENTS
36+
37+
# create a built-in custom desc_sig_element (added to SIG_ELEMENTS)
38+
class BuiltInSigElementLikeNode(addnodes.desc_sig_element, _sig_element=True):
39+
pass
40+
41+
# create a custom desc_sig_element (implicitly not added to SIG_ELEMENTS)
42+
class Custom1SigElementLikeNode(addnodes.desc_sig_element):
43+
pass
44+
45+
# create a custom desc_sig_element (explicitly not added to SIG_ELEMENTS)
46+
class Custom2SigElementLikeNode(addnodes.desc_sig_element, _sig_element=False):
47+
pass
48+
49+
assert BuiltInSigElementLikeNode in addnodes.SIG_ELEMENTS
50+
assert Custom1SigElementLikeNode not in addnodes.SIG_ELEMENTS
51+
assert Custom2SigElementLikeNode not in addnodes.SIG_ELEMENTS

0 commit comments

Comments
 (0)