From d41cae328ec139b4d65b60041f561657cfef86ab Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 13 Apr 2019 23:13:00 +0900 Subject: [PATCH] Add sphinx.util.inspect:isattributedescriptor() --- sphinx/ext/autodoc/__init__.py | 22 ++++++++----------- sphinx/util/inspect.py | 39 ++++++++++++++++++++++++++++++++++ tests/test_util_inspect.py | 27 +++++++++++++++++++++++ 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 4c1032db5..3eeec4523 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1340,17 +1340,14 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): @classmethod def can_document_member(cls, member, membername, isattr, parent): # type: (Any, str, bool, Any) -> bool - non_attr_types = (type, MethodDescriptorType) - isdatadesc = inspect.isdescriptor(member) and not \ - cls.is_function_or_method(member) and not \ - isinstance(member, non_attr_types) and not \ - type(member).__name__ == "instancemethod" - # That last condition addresses an obscure case of C-defined - # methods using a deprecated type in Python 3, that is not otherwise - # exported anywhere by Python - return isdatadesc or (not isinstance(parent, ModuleDocumenter) and - not inspect.isroutine(member) and - not isinstance(member, type)) + if inspect.isattributedescriptor(member): + return True + elif (not isinstance(parent, ModuleDocumenter) and + not inspect.isroutine(member) and + not isinstance(member, type)): + return True + else: + return False def document_members(self, all_members=False): # type: (bool) -> None @@ -1361,8 +1358,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): ret = super().import_object() if inspect.isenumattribute(self.object): self.object = self.object.value - if inspect.isdescriptor(self.object) and \ - not self.is_function_or_method(self.object): + if inspect.isattributedescriptor(self.object): self._datadescriptor = True else: # if it's not a data descriptor diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 0cf7c1084..877f727d4 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -29,6 +29,17 @@ if False: # For type annotation from typing import Any, Callable, Mapping, List, Tuple, Type # NOQA +if sys.version_info > (3, 7): + from types import ( + ClassMethodDescriptorType, + MethodDescriptorType, + WrapperDescriptorType + ) +else: + ClassMethodDescriptorType = type(object.__init__) + MethodDescriptorType = type(str.join) + WrapperDescriptorType = type(dict.__dict__['fromkeys']) + logger = logging.getLogger(__name__) memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE) @@ -161,6 +172,34 @@ def isdescriptor(x): return False +def isattributedescriptor(obj): + # type: (Any) -> bool + """Check if the object is an attribute like descriptor.""" + if inspect.isdatadescriptor(object): + # data descriptor is kind of attribute + return True + elif isdescriptor(obj): + # non data descriptor + if isfunction(obj) or isbuiltin(obj) or inspect.ismethod(obj): + # attribute must not be either function, builtin and method + return False + elif inspect.isclass(obj): + # attribute must not be a class + return False + elif isinstance(obj, (ClassMethodDescriptorType, + MethodDescriptorType, + WrapperDescriptorType)): + # attribute must not be a method descriptor + return False + elif type(obj).__name__ == "instancemethod": + # attribute must not be an instancemethod (C-API) + return False + else: + return True + else: + return False + + def isfunction(obj): # type: (Any) -> bool """Check if the object is function.""" diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index d167c1740..275206526 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -7,8 +7,12 @@ :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ + +import _testcapi +import datetime import functools import sys +import types from textwrap import dedent import pytest @@ -432,3 +436,26 @@ def test_isdescriptor(app): assert inspect.isdescriptor(Base.meth) is True # method of class assert inspect.isdescriptor(Base().meth) is True # method of instance assert inspect.isdescriptor(func) is True # function + + +@pytest.mark.sphinx(testroot='ext-autodoc') +def test_isattributedescriptor(app): + from target.methods import Base + + class Descriptor: + def __get__(self, obj, typ=None): + pass + + testinstancemethod = _testcapi.instancemethod(str.__repr__) + + assert inspect.isattributedescriptor(Base.prop) is True # property + assert inspect.isattributedescriptor(Base.meth) is False # method + assert inspect.isattributedescriptor(Base.staticmeth) is False # staticmethod + assert inspect.isattributedescriptor(Base.classmeth) is False # classmetho + assert inspect.isattributedescriptor(Descriptor) is False # custom descriptor class # NOQA + assert inspect.isattributedescriptor(str.join) is False # MethodDescriptorType # NOQA + assert inspect.isattributedescriptor(object.__init__) is False # WrapperDescriptorType # NOQA + assert inspect.isattributedescriptor(dict.__dict__['fromkeys']) is False # ClassMethodDescriptorType # NOQA + assert inspect.isattributedescriptor(types.FrameType.f_locals) is True # GetSetDescriptorType # NOQA + assert inspect.isattributedescriptor(datetime.timedelta.days) is True # MemberDescriptorType # NOQA + assert inspect.isattributedescriptor(testinstancemethod) is False # instancemethod (C-API) # NOQA