From 5667050dd26d2429c3a00ee35603e1d0fa1e2484 Mon Sep 17 00:00:00 2001
From: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
Date: Wed, 29 Jan 2025 02:34:46 +0000
Subject: [PATCH] Improve the displayed signature for abstract methods (#13271)
---
CHANGES.rst | 6 +
doc/usage/domains/python.rst | 41 ++++++-
sphinx/domains/python/__init__.py | 69 ++++++-----
sphinx/domains/python/_object.py | 3 +-
tests/test_builders/test_build_latex.py | 4 +-
tests/test_domains/test_domain_py.py | 10 +-
.../test_domains/test_domain_py_canonical.py | 6 +-
tests/test_domains/test_domain_py_fields.py | 16 ++-
tests/test_domains/test_domain_py_pyobject.py | 109 ++++++++++++++----
9 files changed, 199 insertions(+), 65 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 4918daf2222..faf6e269a60 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -82,6 +82,12 @@ Features added
Patch by Jonny Saunders and Adam Turner.
* #13172: Add support for short signatures in autosummary.
Patch by Tim Hoffmann.
+* #13271: Change the signature prefix for abstract methods
+ in the Python domain to *abstractmethod* from *abstract*.
+ Patch by Adam Turner.
+* #13271: Support the ``:abstract:`` option for
+ classes, methods, and properties in the Python domain.
+ Patch by Adam Turner.
Bugs fixed
----------
diff --git a/doc/usage/domains/python.rst b/doc/usage/domains/python.rst
index 33cf2b25297..7d7c4e95438 100644
--- a/doc/usage/domains/python.rst
+++ b/doc/usage/domains/python.rst
@@ -230,6 +230,20 @@ The following directives are provided for module and class contents:
.. rubric:: options
+ .. rst:directive:option:: abstract
+ :type: no value
+
+ Indicate that the class is an abstract base class.
+ This produces the following output:
+
+ .. py:class:: Cheese
+ :no-index:
+ :abstract:
+
+ A cheesy representation.
+
+ .. versionadded:: 8.2
+
.. rst:directive:option:: canonical
:type: full qualified name including module name
@@ -320,10 +334,22 @@ The following directives are provided for module and class contents:
.. rubric:: options
- .. rst:directive:option:: abstractmethod
+ .. rst:directive:option:: abstract
+ abstractmethod
:type: no value
Indicate the property is abstract.
+ This produces the following output:
+
+ .. py:property:: Cheese.amount_in_stock
+ :no-index:
+ :abstractmethod:
+
+ Cheese levels at the *National Cheese Emporium*.
+
+ .. versionchanged:: 8.2
+
+ The ``:abstract:`` alias is also supported.
.. rst:directive:option:: classmethod
:type: no value
@@ -412,12 +438,23 @@ The following directives are provided for module and class contents:
.. rubric:: options
- .. rst:directive:option:: abstractmethod
+ .. rst:directive:option:: abstract
+ abstractmethod
:type: no value
Indicate the method is an abstract method.
+ This produces the following output:
+
+ .. py:method:: Cheese.order_more_stock
+ :no-index:
+ :abstractmethod:
+
+ Order more cheese (we're fresh out!).
.. versionadded:: 2.1
+ .. versionchanged:: 8.2
+
+ The ``:abstract:`` alias is also supported.
.. rst:directive:option:: async
:type: no value
diff --git a/sphinx/domains/python/__init__.py b/sphinx/domains/python/__init__.py
index 6ca92900e81..992a8dcb7d3 100644
--- a/sphinx/domains/python/__init__.py
+++ b/sphinx/domains/python/__init__.py
@@ -25,7 +25,7 @@
)
if TYPE_CHECKING:
- from collections.abc import Iterable, Iterator, Set
+ from collections.abc import Iterable, Iterator, Sequence, Set
from typing import Any, ClassVar
from docutils.nodes import Element, Node
@@ -87,14 +87,14 @@ class PyFunction(PyObject):
'async': directives.flag,
})
- def get_signature_prefix(self, sig: str) -> list[nodes.Node]:
+ def get_signature_prefix(self, sig: str) -> Sequence[nodes.Node]:
+ prefix: list[addnodes.desc_sig_element] = []
if 'async' in self.options:
- return [
+ prefix.extend((
addnodes.desc_sig_keyword('', 'async'),
addnodes.desc_sig_space(),
- ]
- else:
- return []
+ ))
+ return prefix
def needs_arglist(self) -> bool:
return True
@@ -186,21 +186,29 @@ class PyClasslike(PyObject):
option_spec: ClassVar[OptionSpec] = PyObject.option_spec.copy()
option_spec.update({
+ 'abstract': directives.flag,
'final': directives.flag,
})
allow_nesting = True
- def get_signature_prefix(self, sig: str) -> list[nodes.Node]:
+ def get_signature_prefix(self, sig: str) -> Sequence[nodes.Node]:
+ prefix: list[addnodes.desc_sig_element] = []
if 'final' in self.options:
- return [
- nodes.Text('final'),
+ prefix.extend((
+ addnodes.desc_sig_keyword('', 'final'),
addnodes.desc_sig_space(),
- nodes.Text(self.objtype),
+ ))
+ if 'abstract' in self.options:
+ prefix.extend((
+ addnodes.desc_sig_keyword('', 'abstract'),
addnodes.desc_sig_space(),
- ]
- else:
- return [nodes.Text(self.objtype), addnodes.desc_sig_space()]
+ ))
+ prefix.extend((
+ addnodes.desc_sig_keyword('', self.objtype),
+ addnodes.desc_sig_space(),
+ ))
+ return prefix
def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str:
if self.objtype == 'class':
@@ -218,6 +226,7 @@ class PyMethod(PyObject):
option_spec: ClassVar[OptionSpec] = PyObject.option_spec.copy()
option_spec.update({
+ 'abstract': directives.flag,
'abstractmethod': directives.flag,
'async': directives.flag,
'classmethod': directives.flag,
@@ -228,31 +237,31 @@ class PyMethod(PyObject):
def needs_arglist(self) -> bool:
return True
- def get_signature_prefix(self, sig: str) -> list[nodes.Node]:
- prefix: list[nodes.Node] = []
+ def get_signature_prefix(self, sig: str) -> Sequence[nodes.Node]:
+ prefix: list[addnodes.desc_sig_element] = []
if 'final' in self.options:
prefix.extend((
- nodes.Text('final'),
+ addnodes.desc_sig_keyword('', 'final'),
addnodes.desc_sig_space(),
))
- if 'abstractmethod' in self.options:
+ if 'abstract' in self.options or 'abstractmethod' in self.options:
prefix.extend((
- nodes.Text('abstract'),
+ addnodes.desc_sig_keyword('', 'abstractmethod'),
addnodes.desc_sig_space(),
))
if 'async' in self.options:
prefix.extend((
- nodes.Text('async'),
+ addnodes.desc_sig_keyword('', 'async'),
addnodes.desc_sig_space(),
))
if 'classmethod' in self.options:
prefix.extend((
- nodes.Text('classmethod'),
+ addnodes.desc_sig_keyword('', 'classmethod'),
addnodes.desc_sig_space(),
))
if 'staticmethod' in self.options:
prefix.extend((
- nodes.Text('static'),
+ addnodes.desc_sig_keyword('', 'static'),
addnodes.desc_sig_space(),
))
return prefix
@@ -373,6 +382,7 @@ class PyProperty(PyObject):
option_spec = PyObject.option_spec.copy()
option_spec.update({
+ 'abstract': directives.flag,
'abstractmethod': directives.flag,
'classmethod': directives.flag,
'type': directives.unchanged,
@@ -394,21 +404,20 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]
return fullname, prefix
- def get_signature_prefix(self, sig: str) -> list[nodes.Node]:
- prefix: list[nodes.Node] = []
- if 'abstractmethod' in self.options:
+ def get_signature_prefix(self, sig: str) -> Sequence[nodes.Node]:
+ prefix: list[addnodes.desc_sig_element] = []
+ if 'abstract' in self.options or 'abstractmethod' in self.options:
prefix.extend((
- nodes.Text('abstract'),
+ addnodes.desc_sig_keyword('', 'abstract'),
addnodes.desc_sig_space(),
))
if 'classmethod' in self.options:
prefix.extend((
- nodes.Text('class'),
+ addnodes.desc_sig_keyword('', 'class'),
addnodes.desc_sig_space(),
))
-
prefix.extend((
- nodes.Text('property'),
+ addnodes.desc_sig_keyword('', 'property'),
addnodes.desc_sig_space(),
))
return prefix
@@ -436,8 +445,8 @@ class PyTypeAlias(PyObject):
'canonical': directives.unchanged,
})
- def get_signature_prefix(self, sig: str) -> list[nodes.Node]:
- return [nodes.Text('type'), addnodes.desc_sig_space()]
+ def get_signature_prefix(self, sig: str) -> Sequence[nodes.Node]:
+ return [addnodes.desc_sig_keyword('', 'type'), addnodes.desc_sig_space()]
def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]:
fullname, prefix = super().handle_signature(sig, signode)
diff --git a/sphinx/domains/python/_object.py b/sphinx/domains/python/_object.py
index b2e8b5e381e..1ed8dc168f0 100644
--- a/sphinx/domains/python/_object.py
+++ b/sphinx/domains/python/_object.py
@@ -25,6 +25,7 @@
)
if TYPE_CHECKING:
+ from collections.abc import Sequence
from typing import ClassVar
from docutils.nodes import Node
@@ -232,7 +233,7 @@ class PyObject(ObjectDescription[tuple[str, str]]):
allow_nesting = False
- def get_signature_prefix(self, sig: str) -> list[nodes.Node]:
+ def get_signature_prefix(self, sig: str) -> Sequence[nodes.Node]:
"""May return a prefix to put before the object name in the
signature.
"""
diff --git a/tests/test_builders/test_build_latex.py b/tests/test_builders/test_build_latex.py
index 0a67e55bc4f..63feb553107 100644
--- a/tests/test_builders/test_build_latex.py
+++ b/tests/test_builders/test_build_latex.py
@@ -2265,7 +2265,7 @@ def test_one_parameter_per_line(app):
# MyGenericClass[X]
assert (
'\\pysiglinewithargsretwithtypelist\n'
- '{\\sphinxbfcode{\\sphinxupquote{class\\DUrole{w}{ }}}'
+ '{\\sphinxbfcode{\\sphinxupquote{\\DUrole{k}{class}\\DUrole{w}{ }}}'
'\\sphinxbfcode{\\sphinxupquote{MyGenericClass}}}\n'
'{\\sphinxtypeparam{\\DUrole{n}{X}}}\n'
'{}\n'
@@ -2275,7 +2275,7 @@ def test_one_parameter_per_line(app):
# MyList[T](list[T])
assert (
'\\pysiglinewithargsretwithtypelist\n'
- '{\\sphinxbfcode{\\sphinxupquote{class\\DUrole{w}{ }}}'
+ '{\\sphinxbfcode{\\sphinxupquote{\\DUrole{k}{class}\\DUrole{w}{ }}}'
'\\sphinxbfcode{\\sphinxupquote{MyList}}}\n'
'{\\sphinxtypeparam{\\DUrole{n}{T}}}\n'
'{\\sphinxparam{list{[}T{]}}}\n'
diff --git a/tests/test_domains/test_domain_py.py b/tests/test_domains/test_domain_py.py
index 1e6d86194a3..e728ba8c315 100644
--- a/tests/test_domains/test_domain_py.py
+++ b/tests/test_domains/test_domain_py.py
@@ -1468,7 +1468,10 @@ def test_class_def_pep_695(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_name, 'Class'],
[
desc_type_parameter_list,
@@ -1530,7 +1533,10 @@ def test_class_def_pep_696(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_name, 'Class'],
[
desc_type_parameter_list,
diff --git a/tests/test_domains/test_domain_py_canonical.py b/tests/test_domains/test_domain_py_canonical.py
index 0c6982d652d..6b47f302a97 100644
--- a/tests/test_domains/test_domain_py_canonical.py
+++ b/tests/test_domains/test_domain_py_canonical.py
@@ -11,6 +11,7 @@
desc_annotation,
desc_content,
desc_name,
+ desc_sig_keyword,
desc_sig_space,
desc_signature,
)
@@ -50,7 +51,10 @@ def test_canonical(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_addname, 'io.'],
[desc_name, 'StringIO'],
),
diff --git a/tests/test_domains/test_domain_py_fields.py b/tests/test_domains/test_domain_py_fields.py
index 285509e9b6e..f1439841930 100644
--- a/tests/test_domains/test_domain_py_fields.py
+++ b/tests/test_domains/test_domain_py_fields.py
@@ -12,6 +12,7 @@
desc_annotation,
desc_content,
desc_name,
+ desc_sig_keyword,
desc_sig_punctuation,
desc_sig_space,
desc_signature,
@@ -51,7 +52,10 @@ def test_info_field_list(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_addname, 'example.'],
[desc_name, 'Class'],
),
@@ -220,7 +224,10 @@ def test_info_field_list_piped_type(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_addname, 'example.'],
[desc_name, 'Class'],
),
@@ -294,7 +301,10 @@ def test_info_field_list_Literal(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_addname, 'example.'],
[desc_name, 'Class'],
),
diff --git a/tests/test_domains/test_domain_py_pyobject.py b/tests/test_domains/test_domain_py_pyobject.py
index 67d91d731e4..671f67db98c 100644
--- a/tests/test_domains/test_domain_py_pyobject.py
+++ b/tests/test_domains/test_domain_py_pyobject.py
@@ -13,6 +13,7 @@
desc_content,
desc_name,
desc_parameterlist,
+ desc_sig_keyword,
desc_sig_punctuation,
desc_sig_space,
desc_signature,
@@ -36,7 +37,10 @@ def test_pyexception_signature(app):
[
desc_signature,
(
- [desc_annotation, ('exception', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'exception'], desc_sig_space),
+ ],
[desc_addname, 'builtins.'],
[desc_name, 'IOError'],
),
@@ -178,7 +182,10 @@ def test_pyobject_prefix(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_name, 'Foo'],
),
],
@@ -247,7 +254,10 @@ def test_pyclass_options(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_name, 'Class1'],
),
],
@@ -263,7 +273,12 @@ def test_pyclass_options(app):
(
[
desc_annotation,
- ('final', desc_sig_space, 'class', desc_sig_space),
+ (
+ [desc_sig_keyword, 'final'],
+ desc_sig_space,
+ [desc_sig_keyword, 'class'],
+ desc_sig_space,
+ ),
],
[desc_name, 'Class2'],
),
@@ -322,7 +337,10 @@ def test_pymethod_options(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_name, 'Class'],
),
],
@@ -376,7 +394,10 @@ def test_pymethod_options(app):
[
desc_signature,
(
- [desc_annotation, ('classmethod', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'classmethod'], desc_sig_space),
+ ],
[desc_name, 'meth2'],
[desc_parameterlist, ()],
),
@@ -399,7 +420,7 @@ def test_pymethod_options(app):
[
desc_signature,
(
- [desc_annotation, ('static', desc_sig_space)],
+ [desc_annotation, ([desc_sig_keyword, 'static'], desc_sig_space)],
[desc_name, 'meth3'],
[desc_parameterlist, ()],
),
@@ -422,7 +443,7 @@ def test_pymethod_options(app):
[
desc_signature,
(
- [desc_annotation, ('async', desc_sig_space)],
+ [desc_annotation, ([desc_sig_keyword, 'async'], desc_sig_space)],
[desc_name, 'meth4'],
[desc_parameterlist, ()],
),
@@ -445,7 +466,10 @@ def test_pymethod_options(app):
[
desc_signature,
(
- [desc_annotation, ('abstract', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'abstractmethod'], desc_sig_space),
+ ],
[desc_name, 'meth5'],
[desc_parameterlist, ()],
),
@@ -468,7 +492,7 @@ def test_pymethod_options(app):
[
desc_signature,
(
- [desc_annotation, ('final', desc_sig_space)],
+ [desc_annotation, ([desc_sig_keyword, 'final'], desc_sig_space)],
[desc_name, 'meth6'],
[desc_parameterlist, ()],
),
@@ -495,7 +519,10 @@ def test_pyclassmethod(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_name, 'Class'],
),
],
@@ -515,7 +542,10 @@ def test_pyclassmethod(app):
[
desc_signature,
(
- [desc_annotation, ('classmethod', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'classmethod'], desc_sig_space),
+ ],
[desc_name, 'meth'],
[desc_parameterlist, ()],
),
@@ -542,7 +572,10 @@ def test_pystaticmethod(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_name, 'Class'],
),
],
@@ -562,7 +595,7 @@ def test_pystaticmethod(app):
[
desc_signature,
(
- [desc_annotation, ('static', desc_sig_space)],
+ [desc_annotation, ([desc_sig_keyword, 'static'], desc_sig_space)],
[desc_name, 'meth'],
[desc_parameterlist, ()],
),
@@ -595,7 +628,10 @@ def test_pyattribute(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_name, 'Class'],
),
],
@@ -673,7 +709,10 @@ def test_pyproperty(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_name, 'Class'],
),
],
@@ -695,7 +734,12 @@ def test_pyproperty(app):
(
[
desc_annotation,
- ('abstract', desc_sig_space, 'property', desc_sig_space),
+ (
+ [desc_sig_keyword, 'abstract'],
+ desc_sig_space,
+ [desc_sig_keyword, 'property'],
+ desc_sig_space,
+ ),
],
[desc_name, 'prop1'],
[
@@ -724,7 +768,12 @@ def test_pyproperty(app):
(
[
desc_annotation,
- ('class', desc_sig_space, 'property', desc_sig_space),
+ (
+ [desc_sig_keyword, 'class'],
+ desc_sig_space,
+ [desc_sig_keyword, 'property'],
+ desc_sig_space,
+ ),
],
[desc_name, 'prop2'],
[
@@ -772,7 +821,10 @@ def test_py_type_alias(app):
[
desc_signature,
(
- [desc_annotation, ('type', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'type'], desc_sig_space),
+ ],
[desc_addname, 'example.'],
[desc_name, 'Alias1'],
[
@@ -803,7 +855,10 @@ def test_py_type_alias(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_addname, 'example.'],
[desc_name, 'Class'],
),
@@ -832,7 +887,7 @@ def test_py_type_alias(app):
[
desc_signature,
(
- [desc_annotation, ('type', desc_sig_space)],
+ [desc_annotation, ([desc_sig_keyword, 'type'], desc_sig_space)],
[desc_name, 'Alias2'],
[
desc_annotation,
@@ -870,7 +925,7 @@ def test_domain_py_type_alias(app):
content = (app.outdir / 'type_alias.html').read_text(encoding='utf8')
assert (
- 'type '
+ 'type '
'module_one.'
'MyAlias'
' ='
@@ -982,7 +1037,10 @@ def test_pycurrentmodule(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_addname, 'Module.'],
[desc_name, 'A'],
),
@@ -1029,7 +1087,10 @@ def test_pycurrentmodule(app):
[
desc_signature,
(
- [desc_annotation, ('class', desc_sig_space)],
+ [
+ desc_annotation,
+ ([desc_sig_keyword, 'class'], desc_sig_space),
+ ],
[desc_addname, 'Other.'],
[desc_name, 'B'],
),