diff --git a/CHANGES b/CHANGES index 440ed870d..e0ab2c872 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,9 @@ Features added Bugs fixed ---------- +* #4415: autodoc classifies inherited classmethods as regular methods +* #4415: autodoc classifies inherited staticmethods as regular methods + Testing -------- diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index d9e82813d..1597d0c1b 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -32,7 +32,7 @@ from sphinx.application import ExtensionError from sphinx.util import logging from sphinx.util.inspect import Signature, isdescriptor, safe_getmembers, \ safe_getattr, object_description, is_builtin_class_method, \ - isenumattribute, getdoc + isenumattribute, isclassmethod, isstaticmethod, getdoc from sphinx.util.docstrings import prepare_docstring if False: @@ -1261,12 +1261,14 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: # to distinguish classmethod/staticmethod obj = self.parent.__dict__.get(self.object_name) + if obj is None: + obj = self.object - if isinstance(obj, classmethod): + if isclassmethod(obj): self.directivetype = 'classmethod' # document class and static members before ordinary ones self.member_order = self.member_order - 1 - elif isinstance(obj, staticmethod): + elif isstaticmethod(obj, cls=self.parent, name=self.object_name): self.directivetype = 'staticmethod' # document class and static members before ordinary ones self.member_order = self.member_order - 1 diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 8c10b7aa5..5ed39906e 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -154,6 +154,40 @@ def isenumattribute(x): return isinstance(x, enum.Enum) +def isclassmethod(obj): + # type: (Any) -> bool + """Check if the object is classmethod.""" + if isinstance(obj, classmethod): + return True + elif inspect.ismethod(obj): + if getattr(obj, 'im_self', None): # py2 + return True + elif getattr(obj, '__self__', None): # py3 + return True + + return False + + +def isstaticmethod(obj, cls=None, name=None): + # type: (Any, Any, unicode) -> bool + """Check if the object is staticmethod.""" + if isinstance(obj, staticmethod): + return True + elif cls and name: + # trace __mro__ if the method is defined in parent class + # + # .. note:: This only works with new style classes. + for basecls in getattr(cls, '__mro__', []): + meth = basecls.__dict__.get(name) + if meth: + if isinstance(meth, staticmethod): + return True + else: + return False + + return False + + def isdescriptor(x): # type: (Any) -> bool """Check if the object is some kind of descriptor.""" diff --git a/tests/roots/test-ext-autodoc/target/__init__.py b/tests/roots/test-ext-autodoc/target/__init__.py index bd00bf183..b657f595f 100644 --- a/tests/roots/test-ext-autodoc/target/__init__.py +++ b/tests/roots/test-ext-autodoc/target/__init__.py @@ -64,6 +64,14 @@ class Base(object): def inheritedmeth(self): """Inherited function.""" + @classmethod + def inheritedclassmeth(cls): + """Inherited class method.""" + + @staticmethod + def inheritedstaticmeth(cls): + """Inherited static method.""" + class Derived(Base): def inheritedmeth(self): diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index d9c94bc0d..55c4b8847 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -715,6 +715,8 @@ def test_generate(): assert_processes(should, 'class', 'Class') options.inherited_members = True should.append(('method', 'target.Class.inheritedmeth')) + should.append(('method', 'target.Class.inheritedclassmeth')) + should.append(('method', 'target.Class.inheritedstaticmeth')) assert_processes(should, 'class', 'Class') # test special members @@ -798,7 +800,9 @@ def test_generate(): ' .. py:attribute:: Class.inst_attr_comment', ' .. py:attribute:: Class.inst_attr_string', ' .. py:attribute:: Class._private_inst_attr', + ' .. py:classmethod:: Class.inheritedclassmeth()', ' .. py:method:: Class.inheritedmeth()', + ' .. py:staticmethod:: Class.inheritedstaticmeth()', ], 'class', 'Class', member_order='bysource', all_members=True) del directive.env.ref_context['py:module']