Skip to content

Commit

Permalink
Merge pull request #6990 from tk0miya/function_signature
Browse files Browse the repository at this point in the history
py domain: Allow to make a style for arguments of functions and methods
  • Loading branch information
tk0miya authored Mar 14, 2020
2 parents 3ad0316 + 50cf68e commit 5c0d043
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 42 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
30 changes: 29 additions & 1 deletion sphinx/addnodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
25 changes: 16 additions & 9 deletions sphinx/domains/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
35 changes: 34 additions & 1 deletion sphinx/transforms/post_transforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand Down Expand Up @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion tests/test_build_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': [
Expand Down
80 changes: 50 additions & 30 deletions tests/test_domain_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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):
Expand All @@ -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.')
Expand All @@ -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):
Expand Down

0 comments on commit 5c0d043

Please sign in to comment.