Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@
('py:exc', 'sphinx.environment.NoUri'),
('py:func', 'setup'),
('py:func', 'sphinx.util.nodes.nested_parse_with_titles'),
('py:class', 'IndexExpression'), # doc/usage/domains/python.rst
# Error in sphinxcontrib.websupport.core::WebSupport.add_comment
('py:meth', 'get_comments'),
('py:mod', 'autodoc'),
Expand Down
79 changes: 79 additions & 0 deletions doc/usage/domains/python.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,45 @@ The following directives are provided for module and class contents:

.. versionadded:: 7.1

.. rst:directive:option:: subscript
:type: no value

Indicate that the function's parameters should be displayed
within square brackets instead of parentheses, in order to
indicate that it must be invoked as a *subscript function*..

For example:

.. code-block:: rst

.. py:function:: numpy.s_(indexing_expr: tuple[int, ...]) -> IndexExpression
:subscript:

Creates an index expression object.

This is rendered as:

.. py:method:: numpy.s_(indexing_expr: tuple[int, ...]) -> IndexExpression
:no-contents-entry:
:no-index-entry:
:subscript:

Creates an index expression object.

A *subscript function* can be implemented as follows:

.. code-block:: python

class _S:
@staticmethod
def __getitem__(indexing_expr: tuple[int, ...]) -> IndexExpression:
...

s_ = _S()

.. versionadded:: 8.3



.. rst:directive:: .. py:data:: name

Expand Down Expand Up @@ -516,6 +555,46 @@ The following directives are provided for module and class contents:

.. versionadded:: 2.1

.. rst:directive:option:: subscript
:type: no value

Indicate that the method's parameters should be displayed within
square brackets instead of parentheses, in order to indicate
that it must be invoked as a *subscript method*.

For example:

.. code-block:: rst

.. py:method:: Array.vindex(self, indexing_expr: tuple[int, ...]) -> list[int]
:subscript:

Index the array using *vindex* semantics.

This is rendered as:

.. py:method:: Array.vindex(self, indexing_expr: tuple[int, ...]) -> list[int]
:no-contents-entry:
:no-index-entry:
:subscript:

Index the array using *vindex* semantics.

A *subscript method* can be implemented as follows:

.. code-block:: python

class Array:
class _Vindex:
def __init__(self, parent: Array):
self.parent = parent
def __getitem__(self, indexing_expr: tuple[int, ...]) -> list[int]:
...
@property
def vindex(self) -> Array._Vindex:
return Array._Vindex(self)

.. versionadded:: 8.3

.. rst:directive:: .. py:staticmethod:: name(parameters)
.. py:staticmethod:: name[type parameters](parameters)
Expand Down
10 changes: 9 additions & 1 deletion sphinx/addnodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,20 @@ class desc_parameterlist(nodes.Part, nodes.Inline, nodes.FixedTextElement):
In that case each parameter will then be written on its own, indented line.
A trailing comma will be added on the last line
if ``multi_line_trailing_comma`` is True.

By default, it is surrounded by parentheses ``("(", ")")``, but this may be
overridden by specifying a ``brackets`` attribute.
"""

child_text_separator = ', '

@property
def brackets(self) -> tuple[str, str]:
return self.get('brackets', ('(', ')'))

def astext(self) -> str:
return f'({super().astext()})'
open_punct, close_punct = self.brackets
return f'{open_punct}{super().astext()}{close_punct}'


class desc_type_parameter_list(nodes.Part, nodes.Inline, nodes.FixedTextElement):
Expand Down
10 changes: 10 additions & 0 deletions sphinx/domains/python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class PyFunction(PyObject):
option_spec: ClassVar[OptionSpec] = PyObject.option_spec.copy()
option_spec.update({
'async': directives.flag,
'subscript': directives.flag,
})

def get_signature_prefix(self, sig: str) -> Sequence[nodes.Node]:
Expand Down Expand Up @@ -235,6 +236,7 @@ class PyMethod(PyObject):
'classmethod': directives.flag,
'final': directives.flag,
'staticmethod': directives.flag,
'subscript': directives.flag,
})

def needs_arglist(self) -> bool:
Expand Down Expand Up @@ -293,6 +295,10 @@ class PyClassMethod(PyMethod):
"""Description of a classmethod."""

option_spec: ClassVar[OptionSpec] = PyObject.option_spec.copy()
option_spec.update({
'async': directives.flag,
'subscript': directives.flag,
})

def run(self) -> list[Node]:
self.name = 'py:method'
Expand All @@ -305,6 +311,10 @@ class PyStaticMethod(PyMethod):
"""Description of a staticmethod."""

option_spec: ClassVar[OptionSpec] = PyObject.option_spec.copy()
option_spec.update({
'async': directives.flag,
'subscript': directives.flag,
})

def run(self) -> list[Node]:
self.name = 'py:method'
Expand Down
4 changes: 4 additions & 0 deletions sphinx/domains/python/_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,10 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]
# for callables, add an empty parameter list
signode += addnodes.desc_parameterlist()

if 'subscript' in self.options:
for node in signode.findall(addnodes.desc_parameterlist):
node['brackets'] = '[', ']'

if retann:
children = _parse_annotation(retann, self.env)
signode += addnodes.desc_returns(retann, '', *children)
Expand Down
4 changes: 2 additions & 2 deletions sphinx/texinputs/sphinxlatexobjects.sty
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@
\setlength\sphinxsignaturelistskip{0pt}
\newcommand{\pysigtypelistopen}{\hskip\sphinxsignaturelistskip\sphinxcode{[}}
\newcommand{\pysigtypelistclose}{\sphinxcode{]}}
\newcommand{\pysigarglistopen}{\hskip\sphinxsignaturelistskip\sphinxcode{(}}
\newcommand{\pysigarglistclose}{\sphinxcode{)}}
\newcommand{\pysigarglistopen}{\hskip\sphinxsignaturelistskip\sphinxcode{\pysigarglistopenpunct}}
\newcommand{\pysigarglistclose}{\sphinxcode{\pysigarglistclosepunct}}
%
% Use a \parbox to accommodate long argument list in signatures
% LaTeX did not imagine that an \item label could need multi-line rendering
Expand Down
5 changes: 4 additions & 1 deletion sphinx/writers/html5.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,10 @@ def _depart_sig_parameter_list(self, node: Element) -> None:
self.body.append(f'<span class="sig-paren">{sig_close_paren}</span>')

def visit_desc_parameterlist(self, node: Element) -> None:
self._visit_sig_parameter_list(node, addnodes.desc_parameter, '(', ')')
open_punct, close_punct = node.brackets # type: ignore[attr-defined]
self._visit_sig_parameter_list(
node, addnodes.desc_parameter, open_punct, close_punct
)

def depart_desc_parameterlist(self, node: Element) -> None:
self._depart_sig_parameter_list(node)
Expand Down
12 changes: 12 additions & 0 deletions sphinx/writers/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,16 @@ def depart_desc(self, node: Element) -> None:
else:
self.body.append(CR + r'\end{fulllineitems}' + BLANKLINE)

def _define_parameterlist_brackets(self, open_punct: str, close_punct: str) -> None:
self.body.append(
r'\def\pysigarglistopenpunct{'
+ self.escape(open_punct)
+ '}'
+ r'\def\pysigarglistclosepunct{'
+ self.escape(close_punct)
+ '}'
)

def _visit_signature_line(self, node: Element) -> None:
def next_sibling(e: Node) -> Node | None:
try:
Expand All @@ -837,6 +847,7 @@ def has_multi_line(e: Element) -> bool:
if isinstance(arglist, addnodes.desc_parameterlist):
# tp_list + arglist: \macro{name}{tp_list}{arglist}{retann}
multi_arglist = has_multi_line(arglist)
self._define_parameterlist_brackets(*arglist.brackets)
else:
# orphan tp_list: \macro{name}{tp_list}{}{retann}
# see: https://github.com/sphinx-doc/sphinx/issues/12543
Expand Down Expand Up @@ -867,6 +878,7 @@ def has_multi_line(e: Element) -> bool:
break

if isinstance(child, addnodes.desc_parameterlist):
self._define_parameterlist_brackets(*child.brackets)
# arglist only: \macro{name}{arglist}{retann}
if has_multi_line(child):
self.body.append(CR + r'\pysigwithonelineperarg' + CR + '{')
Expand Down
6 changes: 4 additions & 2 deletions sphinx/writers/manpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,13 @@ def depart_desc_returns(self, node: Element) -> None:
pass

def visit_desc_parameterlist(self, node: Element) -> None:
self.body.append('(')
open_punct, _ = node.brackets # type: ignore[attr-defined]
self.body.append(open_punct)
self.first_param = 1

def depart_desc_parameterlist(self, node: Element) -> None:
self.body.append(')')
_, close_punct = node.brackets # type: ignore[attr-defined]
self.body.append(close_punct)

def visit_desc_type_parameter_list(self, node: Element) -> None:
self.body.append('[')
Expand Down
6 changes: 4 additions & 2 deletions sphinx/writers/texinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -1480,11 +1480,13 @@ def depart_desc_returns(self, node: Element) -> None:
pass

def visit_desc_parameterlist(self, node: Element) -> None:
self.body.append(' (')
open_punct, _ = node.brackets # type: ignore[attr-defined]
self.body.append(f' {open_punct}')
self.first_param = 1

def depart_desc_parameterlist(self, node: Element) -> None:
self.body.append(')')
_, close_punct = node.brackets # type: ignore[attr-defined]
self.body.append(close_punct)

def visit_desc_type_parameter_list(self, node: Element) -> None:
self.body.append(' [')
Expand Down
5 changes: 4 additions & 1 deletion sphinx/writers/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,10 @@ def _depart_sig_parameter_list(self, node: Element) -> None:
self.add_text(sig_close_paren)

def visit_desc_parameterlist(self, node: Element) -> None:
self._visit_sig_parameter_list(node, addnodes.desc_parameter, '(', ')')
open_punct, close_punct = node.brackets # type: ignore[attr-defined]
self._visit_sig_parameter_list(
node, addnodes.desc_parameter, open_punct, close_punct
)

def depart_desc_parameterlist(self, node: Element) -> None:
self._depart_sig_parameter_list(node)
Expand Down
9 changes: 9 additions & 0 deletions tests/test_domains/test_domain_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -1819,3 +1819,12 @@ def test_pytype_canonical(app):

doctree = restructuredtext.parse(app, text)
assert not app.warning.getvalue()


def test_subscript_function(app):
text = """
.. py:function:: f(x: int) -> bool
:subscript:
"""
doctree = restructuredtext.parse(app, text)
assert doctree.astext().strip() == 'f[x: int] -> bool'
Loading