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'], ),