diff --git a/CHANGES b/CHANGES index 6d043f802..a36988937 100644 --- a/CHANGES +++ b/CHANGES @@ -33,6 +33,7 @@ Deprecated * ``sphinx.directives.TabularColumns`` * ``sphinx.directives.TocTree`` * ``sphinx.directives.VersionChange`` +* ``sphinx.domains.python.PyClassmember`` * ``sphinx.domains.std.StandardDomain._resolve_citation_xref()`` * ``sphinx.domains.std.StandardDomain.note_citations()`` * ``sphinx.domains.std.StandardDomain.note_citation_refs()`` diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index beeafab08..ffe0bdccb 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -116,6 +116,14 @@ The following is a list of deprecated interfaces. - 4.0 - ``sphinx.directives.other.VersionChange`` + * - ``sphinx.domains.python.PyClassmember`` + - 2.1 + - 4.0 + - ``sphinx.domains.python.PyAttribute``, + ``sphinx.domains.python.PyMethod``, + ``sphinx.domains.python.PyClassMethod`` and + ``sphinx.domains.python.PyStaticMethod`` + * - ``sphinx.domains.std.StandardDomain._resolve_citation_xref()`` - 2.1 - 4.0 diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 29c0c2536..de31eef00 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -9,13 +9,16 @@ """ import re +import warnings from typing import cast from docutils import nodes from docutils.parsers.rst import directives from sphinx import addnodes, locale -from sphinx.deprecation import DeprecatedDict, RemovedInSphinx30Warning +from sphinx.deprecation import ( + DeprecatedDict, RemovedInSphinx30Warning, RemovedInSphinx40Warning +) from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType, Index, IndexEntry from sphinx.locale import _, __ @@ -453,6 +456,13 @@ class PyClassmember(PyObject): Description of a class member (methods, attributes). """ + def run(self): + # type: () -> List[nodes.Node] + warnings.warn('PyClassmember is deprecated.', + RemovedInSphinx40Warning) + + return super().run() + def needs_arglist(self): # type: () -> bool return self.objtype.endswith('method') @@ -523,6 +533,94 @@ class PyClassmember(PyObject): return '' +class PyMethod(PyObject): + """Description of a method.""" + + def needs_arglist(self): + # type: () -> bool + return True + + def get_index_text(self, modname, name_cls): + # type: (str, Tuple[str, str]) -> str + name, cls = name_cls + try: + clsname, methname = name.rsplit('.', 1) + if modname and self.env.config.add_module_names: + clsname = '.'.join([modname, clsname]) + except ValueError: + if modname: + return _('%s() (in module %s)') % (name, modname) + else: + return '%s()' % name + + return _('%s() (%s method)') % (methname, clsname) + + +class PyClassMethod(PyMethod): + """Description of a classmethod.""" + + def get_signature_prefix(self, sig): + # type: (str) -> str + return 'classmethod ' + + def get_index_text(self, modname, name_cls): + # type: (str, Tuple[str, str]) -> str + name, cls = name_cls + try: + clsname, methname = name.rsplit('.', 1) + if modname and self.env.config.add_module_names: + clsname = '.'.join([modname, clsname]) + except ValueError: + if modname: + return _('%s() (in module %s)') % (name, modname) + else: + return '%s()' % name + + return _('%s() (%s class method)') % (methname, clsname) + + +class PyStaticMethod(PyMethod): + """Description of a staticmethod.""" + + def get_signature_prefix(self, sig): + # type: (str) -> str + return 'static ' + + def get_index_text(self, modname, name_cls): + # type: (str, Tuple[str, str]) -> str + name, cls = name_cls + try: + clsname, methname = name.rsplit('.', 1) + if modname and self.env.config.add_module_names: + clsname = '.'.join([modname, clsname]) + except ValueError: + if modname: + return _('%s() (in module %s)') % (name, modname) + else: + return '%s()' % name + + return _('%s() (%s static method)') % (methname, clsname) + + +class PyAttribute(PyObject): + """Description of an attribute.""" + + def get_index_text(self, modname, name_cls): + # type: (str, Tuple[str, str]) -> str + name, cls = name_cls + try: + clsname, attrname = name.rsplit('.', 1) + if modname and self.env.config.add_module_names: + clsname = '.'.join([modname, clsname]) + except ValueError: + if modname: + return _('%s (in module %s)') % (name, modname) + else: + return name + + return _('%s (%s attribute)') % (attrname, clsname) + + class PyDecoratorMixin: """ Mixin for decorator directives. @@ -745,10 +843,10 @@ class PythonDomain(Domain): 'data': PyModulelevel, 'class': PyClasslike, 'exception': PyClasslike, - 'method': PyClassmember, - 'classmethod': PyClassmember, - 'staticmethod': PyClassmember, - 'attribute': PyClassmember, + 'method': PyMethod, + 'classmethod': PyClassMethod, + 'staticmethod': PyStaticMethod, + 'attribute': PyAttribute, 'module': PyModule, 'currentmodule': PyCurrentModule, 'decorator': PyDecoratorFunction, diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index fb6e70914..afc34a697 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -290,3 +290,85 @@ def test_pyobject_prefix(app): desc)])])) assert doctree[1][1][1].astext().strip() == 'say' # prefix is stripped assert doctree[1][1][3].astext().strip() == 'FooBar.say' # not stripped + + +def test_pymethod(app): + text = (".. py:class:: Class\n" + "\n" + " .. py:method:: meth\n") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, "class "], + [desc_name, "Class"])], + [desc_content, (addnodes.index, + desc)])])) + + assert_node(doctree[1][1][0], addnodes.index, + entries=[('single', 'meth() (Class method)', 'Class.meth', '', None)]) + assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "meth"], + [desc_parameterlist, ()])], + [desc_content, ()])) + assert 'Class.meth' in domain.objects + assert domain.objects['Class.meth'] == ('index', 'method') + + +def test_pyclassmethod(app): + text = (".. py:class:: Class\n" + "\n" + " .. py:classmethod:: meth\n") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, "class "], + [desc_name, "Class"])], + [desc_content, (addnodes.index, + desc)])])) + assert_node(doctree[1][1][0], addnodes.index, + entries=[('single', 'meth() (Class class method)', 'Class.meth', '', None)]) + assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, "classmethod "], + [desc_name, "meth"], + [desc_parameterlist, ()])], + [desc_content, ()])) + assert 'Class.meth' in domain.objects + assert domain.objects['Class.meth'] == ('index', 'classmethod') + + +def test_pystaticmethod(app): + text = (".. py:class:: Class\n" + "\n" + " .. py:staticmethod:: meth\n") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, "class "], + [desc_name, "Class"])], + [desc_content, (addnodes.index, + desc)])])) + assert_node(doctree[1][1][0], addnodes.index, + entries=[('single', 'meth() (Class static method)', 'Class.meth', '', None)]) + assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, "static "], + [desc_name, "meth"], + [desc_parameterlist, ()])], + [desc_content, ()])) + assert 'Class.meth' in domain.objects + assert domain.objects['Class.meth'] == ('index', 'staticmethod') + + +def test_pyattribute(app): + text = (".. py:class:: Class\n" + "\n" + " .. py:attribute:: attr\n") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, "class "], + [desc_name, "Class"])], + [desc_content, (addnodes.index, + desc)])])) + assert_node(doctree[1][1][0], addnodes.index, + entries=[('single', 'attr (Class attribute)', 'Class.attr', '', None)]) + assert_node(doctree[1][1][1], ([desc_signature, desc_name, "attr"], + [desc_content, ()])) + assert 'Class.attr' in domain.objects + assert domain.objects['Class.attr'] == ('index', 'attribute')