Add sphinx.util.inspect:isattributedescriptor()

This commit is contained in:
Takeshi KOMIYA 2019-04-13 23:13:00 +09:00
parent 91fac1a0c3
commit d41cae328e
3 changed files with 75 additions and 13 deletions

View File

@ -1340,17 +1340,14 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member, membername, isattr, parent):
# type: (Any, str, bool, Any) -> bool # type: (Any, str, bool, Any) -> bool
non_attr_types = (type, MethodDescriptorType) if inspect.isattributedescriptor(member):
isdatadesc = inspect.isdescriptor(member) and not \ return True
cls.is_function_or_method(member) and not \ elif (not isinstance(parent, ModuleDocumenter) and
isinstance(member, non_attr_types) and not \ not inspect.isroutine(member) and
type(member).__name__ == "instancemethod" not isinstance(member, type)):
# That last condition addresses an obscure case of C-defined return True
# methods using a deprecated type in Python 3, that is not otherwise else:
# exported anywhere by Python return False
return isdatadesc or (not isinstance(parent, ModuleDocumenter) and
not inspect.isroutine(member) and
not isinstance(member, type))
def document_members(self, all_members=False): def document_members(self, all_members=False):
# type: (bool) -> None # type: (bool) -> None
@ -1361,8 +1358,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
ret = super().import_object() ret = super().import_object()
if inspect.isenumattribute(self.object): if inspect.isenumattribute(self.object):
self.object = self.object.value self.object = self.object.value
if inspect.isdescriptor(self.object) and \ if inspect.isattributedescriptor(self.object):
not self.is_function_or_method(self.object):
self._datadescriptor = True self._datadescriptor = True
else: else:
# if it's not a data descriptor # if it's not a data descriptor

View File

@ -29,6 +29,17 @@ if False:
# For type annotation # For type annotation
from typing import Any, Callable, Mapping, List, Tuple, Type # NOQA 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__) logger = logging.getLogger(__name__)
memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE) memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE)
@ -161,6 +172,34 @@ def isdescriptor(x):
return False 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): def isfunction(obj):
# type: (Any) -> bool # type: (Any) -> bool
"""Check if the object is function.""" """Check if the object is function."""

View File

@ -7,8 +7,12 @@
:copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import _testcapi
import datetime
import functools import functools
import sys import sys
import types
from textwrap import dedent from textwrap import dedent
import pytest 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 class
assert inspect.isdescriptor(Base().meth) is True # method of instance assert inspect.isdescriptor(Base().meth) is True # method of instance
assert inspect.isdescriptor(func) is True # function 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