From 89c446eaf74b0f6cadb7fb42b9bcc29e9eab7e15 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 24 Feb 2020 01:23:07 +0900 Subject: [PATCH 1/2] Add desc_sig_element and inherited nodes --- sphinx/addnodes.py | 30 +++++++++++++++- sphinx/transforms/post_transforms/__init__.py | 35 ++++++++++++++++++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py index 15d5fc46be2..fa04e9344a3 100644 --- a/sphinx/addnodes.py +++ b/sphinx/addnodes.py @@ -12,7 +12,7 @@ from typing import Any, Dict, List, Sequence from docutils import nodes -from docutils.nodes import Node +from docutils.nodes import Element, Node from sphinx.deprecation import RemovedInSphinx40Warning @@ -174,6 +174,31 @@ class desc_content(nodes.General, nodes.Element): """ +class desc_sig_element(nodes.inline): + """Common parent class of nodes for inline text of a signature.""" + classes = [] # type: List[str] + + def __init__(self, rawsource: str = '', text: str = '', + *children: Element, **attributes: Any) -> None: + super().__init__(rawsource, text, *children, **attributes) + self['classes'].extend(self.classes) + + +class desc_sig_name(desc_sig_element): + """Node for a name in a signature.""" + classes = ["n"] + + +class desc_sig_operator(desc_sig_element): + """Node for an operator in a signature.""" + classes = ["o"] + + +class desc_sig_punctuation(desc_sig_element): + """Node for a punctuation in a signature.""" + classes = ["p"] + + # new admonition-like constructs class versionmodified(nodes.Admonition, nodes.TextElement): @@ -332,6 +357,9 @@ def setup(app: "Sphinx") -> Dict[str, Any]: app.add_node(desc_optional) app.add_node(desc_annotation) app.add_node(desc_content) + app.add_node(desc_sig_name) + app.add_node(desc_sig_operator) + app.add_node(desc_sig_punctuation) app.add_node(versionmodified) app.add_node(seealso) app.add_node(productionlist) diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py index 6d7c3b0eb0f..48f5dc24821 100644 --- a/sphinx/transforms/post_transforms/__init__.py +++ b/sphinx/transforms/post_transforms/__init__.py @@ -8,7 +8,7 @@ :license: BSD, see LICENSE for details. """ -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Tuple, Type from typing import cast from docutils import nodes @@ -22,6 +22,7 @@ from sphinx.locale import __ from sphinx.transforms import SphinxTransform from sphinx.util import logging +from sphinx.util.docutils import SphinxTranslator from sphinx.util.nodes import process_only_nodes @@ -186,9 +187,41 @@ def run(self, **kwargs: Any) -> None: process_only_nodes(self.document, self.app.builder.tags) +class SigElementFallbackTransform(SphinxPostTransform): + """Fallback desc_sig_element nodes to inline if translator does not supported them.""" + default_priority = 200 + + SIG_ELEMENTS = [addnodes.desc_sig_name, + addnodes.desc_sig_operator, + addnodes.desc_sig_punctuation] + + def run(self, **kwargs: Any) -> None: + def has_visitor(translator: Type[nodes.NodeVisitor], node: Type[Element]) -> bool: + return hasattr(translator, "visit_%s" % node.__name__) + + translator = self.app.builder.get_translator_class() + if isinstance(translator, SphinxTranslator): + # subclass of SphinxTranslator supports desc_sig_element nodes automatically. + return + + if all(has_visitor(translator, node) for node in self.SIG_ELEMENTS): + # the translator supports all desc_sig_element nodes + return + else: + self.fallback() + + def fallback(self): + for node in self.document.traverse(addnodes.desc_sig_element): + newnode = nodes.inline() + newnode.update_all_atts(node) + newnode.extend(node) + node.replace_self(newnode) + + def setup(app: Sphinx) -> Dict[str, Any]: app.add_post_transform(ReferencesResolver) app.add_post_transform(OnlyNodeTransform) + app.add_post_transform(SigElementFallbackTransform) return { 'version': 'builtin', From 50cf68e0d4a910ffa1f3c6276fc7d32e85c5e1d3 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 4 Jan 2020 23:48:37 +0900 Subject: [PATCH 2/2] py domain: Allow to make a style for arguments of functions and methods (refs: #6417) --- CHANGES | 3 ++ sphinx/domains/python.py | 25 ++++++++----- tests/test_build_html.py | 3 +- tests/test_domain_py.py | 80 +++++++++++++++++++++++++--------------- 4 files changed, 71 insertions(+), 40 deletions(-) diff --git a/CHANGES b/CHANGES index aaf2ac743ae..27e8599d6c1 100644 --- a/CHANGES +++ b/CHANGES @@ -19,6 +19,8 @@ Incompatible changes when ``:inherited-members:`` and ``:special-members:`` are given. * #6830: py domain: ``meta`` fields in info-field-list becomes reserved. They are not displayed on output document now +* #6417: py domain: doctree of desc_parameterlist has been changed. The + argument names, annotations and default values are wrapped with inline node * The structure of ``sphinx.events.EventManager.listeners`` has changed * Due to the scoping changes for :rst:dir:`productionlist` some uses of :rst:role:`token` must be modified to include the scope which was previously @@ -67,6 +69,7 @@ Features added * #6830: py domain: Add new event: :event:`object-description-transform` * #6895: py domain: Do not emit nitpicky warnings for built-in types * py domain: Support lambda functions in function signature +* #6417: py domain: Allow to make a style for arguments of functions and methods * Support priority of event handlers. For more detail, see :py:meth:`.Sphinx.connect()` * #3077: Implement the scoping for :rst:dir:`productionlist` as indicated diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index cd0a59b917e..72bde77a3b3 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -75,35 +75,42 @@ def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist: for param in sig.parameters.values(): if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY: # PEP-570: Separator for Positional Only Parameter: / - params += addnodes.desc_parameter('', nodes.Text('/')) + params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '/')) if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY, None): # PEP-3102: Separator for Keyword Only Parameter: * - params += addnodes.desc_parameter('', nodes.Text('*')) + params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '*')) node = addnodes.desc_parameter() if param.kind == param.VAR_POSITIONAL: - node += nodes.Text('*' + param.name) + node += addnodes.desc_sig_operator('', '*') + node += addnodes.desc_sig_name('', param.name) elif param.kind == param.VAR_KEYWORD: - node += nodes.Text('**' + param.name) + node += addnodes.desc_sig_operator('', '**') + node += addnodes.desc_sig_name('', param.name) else: - node += nodes.Text(param.name) + node += addnodes.desc_sig_name('', param.name) if param.annotation is not param.empty: - node += nodes.Text(': ' + param.annotation) + node += addnodes.desc_sig_punctuation('', ':') + node += nodes.Text(' ') + node += addnodes.desc_sig_name('', param.annotation) if param.default is not param.empty: if param.annotation is not param.empty: - node += nodes.Text(' = ' + str(param.default)) + node += nodes.Text(' ') + node += addnodes.desc_sig_operator('', '=') + node += nodes.Text(' ') else: - node += nodes.Text('=' + str(param.default)) + node += addnodes.desc_sig_operator('', '=') + node += nodes.inline('', param.default, classes=['default_value']) params += node last_kind = param.kind if last_kind == Parameter.POSITIONAL_ONLY: # PEP-570: Separator for Positional Only Parameter: / - params += addnodes.desc_parameter('', nodes.Text('/')) + params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '/')) return params diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 39cb3bf714f..4a77c921ca1 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -177,7 +177,8 @@ def test_html4_output(app, status, warning): ], 'autodoc.html': [ (".//dl[@class='py class']/dt[@id='autodoc-target-class']", ''), - (".//dl[@class='py function']/dt[@id='autodoc-target-function']/em", r'\*\*kwds'), + (".//dl[@class='py function']/dt[@id='autodoc-target-function']/em/span", r'\*\*'), + (".//dl[@class='py function']/dt[@id='autodoc-target-function']/em/span", r'kwds'), (".//dd/p", r'Return spam\.'), ], 'extapi.html': [ diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 27819af6b4f..b3a510a8bd7 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -17,7 +17,8 @@ from sphinx import addnodes from sphinx.addnodes import ( desc, desc_addname, desc_annotation, desc_content, desc_name, desc_optional, - desc_parameter, desc_parameterlist, desc_returns, desc_signature + desc_parameter, desc_parameterlist, desc_returns, desc_signature, + desc_sig_name, desc_sig_operator, desc_sig_punctuation, ) from sphinx.domains import IndexEntry from sphinx.domains.python import ( @@ -246,8 +247,10 @@ def test_pyfunction_signature(app): assert_node(doctree[1], addnodes.desc, desctype="function", domain="py", objtype="function", noindex=False) assert_node(doctree[1][0][1], - [desc_parameterlist, desc_parameter, ("name", - ": str")]) + [desc_parameterlist, desc_parameter, ([desc_sig_name, "name"], + [desc_sig_punctuation, ":"], + " ", + [nodes.inline, "str"])]) def test_pyfunction_signature_full(app): @@ -262,17 +265,31 @@ def test_pyfunction_signature_full(app): assert_node(doctree[1], addnodes.desc, desctype="function", domain="py", objtype="function", noindex=False) assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, ("a", - ": str")], - [desc_parameter, ("b", - "=1")], - [desc_parameter, ("*args", - ": str")], - [desc_parameter, ("c", - ": bool", - " = True")], - [desc_parameter, ("**kwargs", - ": str")])]) + [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "a"], + [desc_sig_punctuation, ":"], + " ", + [desc_sig_name, "str"])], + [desc_parameter, ([desc_sig_name, "b"], + [desc_sig_operator, "="], + [nodes.inline, "1"])], + [desc_parameter, ([desc_sig_operator, "*"], + [desc_sig_name, "args"], + [desc_sig_punctuation, ":"], + " ", + [desc_sig_name, "str"])], + [desc_parameter, ([desc_sig_name, "c"], + [desc_sig_punctuation, ":"], + " ", + [desc_sig_name, "bool"], + " ", + [desc_sig_operator, "="], + " ", + [nodes.inline, "True"])], + [desc_parameter, ([desc_sig_operator, "**"], + [desc_sig_name, "kwargs"], + [desc_sig_punctuation, ":"], + " ", + [desc_sig_name, "str"])])]) @pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') @@ -281,37 +298,40 @@ def test_pyfunction_signature_full_py38(app): text = ".. py:function:: hello(*, a)" doctree = restructuredtext.parse(app, text) assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, "*"], - [desc_parameter, ("a", - "=None")])]) + [desc_parameterlist, ([desc_parameter, nodes.inline, "*"], + [desc_parameter, ([desc_sig_name, "a"], + [desc_sig_operator, "="], + [nodes.inline, "None"])])]) # case: separator in the middle text = ".. py:function:: hello(a, /, b, *, c)" doctree = restructuredtext.parse(app, text) assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, "a"], - [desc_parameter, "/"], - [desc_parameter, "b"], - [desc_parameter, "*"], - [desc_parameter, ("c", - "=None")])]) + [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"], + [desc_parameter, desc_sig_operator, "/"], + [desc_parameter, desc_sig_name, "b"], + [desc_parameter, desc_sig_operator, "*"], + [desc_parameter, ([desc_sig_name, "c"], + [desc_sig_operator, "="], + [nodes.inline, "None"])])]) # case: separator in the middle (2) text = ".. py:function:: hello(a, /, *, b)" doctree = restructuredtext.parse(app, text) assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, "a"], - [desc_parameter, "/"], - [desc_parameter, "*"], - [desc_parameter, ("b", - "=None")])]) + [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"], + [desc_parameter, desc_sig_operator, "/"], + [desc_parameter, desc_sig_operator, "*"], + [desc_parameter, ([desc_sig_name, "b"], + [desc_sig_operator, "="], + [nodes.inline, "None"])])]) # case: separator at tail text = ".. py:function:: hello(a, /)" doctree = restructuredtext.parse(app, text) assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, "a"], - [desc_parameter, "/"])]) + [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"], + [desc_parameter, desc_sig_operator, "/"])]) def test_optional_pyfunction_signature(app):