Skip to content

Commit

Permalink
Refactor sphinx.util.inspect and tests (#11527)
Browse files Browse the repository at this point in the history
  • Loading branch information
AA-Turner authored Jul 28, 2023
1 parent c9d4a1a commit 7cce00a
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 64 deletions.
35 changes: 15 additions & 20 deletions sphinx/util/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,11 @@

def unwrap(obj: Any) -> Any:
"""Get an original object from wrapped object (wrapped functions)."""
if hasattr(obj, '__sphinx_mock__'):
# Skip unwrapping mock object to avoid RecursionError
return obj
try:
if hasattr(obj, '__sphinx_mock__'):
# Skip unwrapping mock object to avoid RecursionError
return obj
else:
return inspect.unwrap(obj)
return inspect.unwrap(obj)
except ValueError:
# might be a mock object
return obj
Expand Down Expand Up @@ -81,11 +80,9 @@ def getall(obj: Any) -> Sequence[str] | None:
__all__ = safe_getattr(obj, '__all__', None)
if __all__ is None:
return None
else:
if (isinstance(__all__, (list, tuple)) and all(isinstance(e, str) for e in __all__)):
return __all__
else:
raise ValueError(__all__)
if isinstance(__all__, (list, tuple)) and all(isinstance(e, str) for e in __all__):
return __all__
raise ValueError(__all__)


def getannotations(obj: Any) -> Mapping[str, Any]:
Expand Down Expand Up @@ -157,10 +154,9 @@ def isNewType(obj: Any) -> bool:
"""Check the if object is a kind of NewType."""
if sys.version_info[:2] >= (3, 10):
return isinstance(obj, typing.NewType)
else:
__module__ = safe_getattr(obj, '__module__', None)
__qualname__ = safe_getattr(obj, '__qualname__', None)
return __module__ == 'typing' and __qualname__ == 'NewType.<locals>.new_type'
__module__ = safe_getattr(obj, '__module__', None)
__qualname__ = safe_getattr(obj, '__qualname__', None)
return __module__ == 'typing' and __qualname__ == 'NewType.<locals>.new_type'


def isenumclass(x: Any) -> bool:
Expand Down Expand Up @@ -209,15 +205,14 @@ def isstaticmethod(obj: Any, cls: Any = None, name: str | None = None) -> bool:
"""Check if the object is staticmethod."""
if isinstance(obj, staticmethod):
return True
elif cls and name:
if cls and name:
# trace __mro__ if the method is defined in parent class
#
# .. note:: This only works well with new style classes.
for basecls in getattr(cls, '__mro__', [cls]):
meth = basecls.__dict__.get(name)
if meth:
return isinstance(meth, staticmethod)

return False


Expand Down Expand Up @@ -291,17 +286,17 @@ def is_singledispatch_method(obj: Any) -> bool:

def isfunction(obj: Any) -> bool:
"""Check if the object is function."""
return inspect.isfunction(unwrap_all(obj))
return inspect.isfunction(unpartial(obj))


def isbuiltin(obj: Any) -> bool:
"""Check if the object is builtin."""
return inspect.isbuiltin(unwrap_all(obj))
"""Check if the object is function."""
return inspect.isbuiltin(unpartial(obj))


def isroutine(obj: Any) -> bool:
"""Check is any kind of function or method."""
return inspect.isroutine(unwrap_all(obj))
return inspect.isroutine(unpartial(obj))


def iscoroutinefunction(obj: Any) -> bool:
Expand Down
141 changes: 97 additions & 44 deletions tests/test_util_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import sys
import types
from inspect import Parameter
from typing import Optional
from typing import Callable, List, Optional, Union # NoQA: UP035

import pytest

Expand All @@ -18,6 +18,74 @@
from sphinx.util.typing import stringify_annotation


class Base:
def meth(self):
pass

@staticmethod
def staticmeth():
pass

@classmethod
def classmeth(cls):
pass

@property
def prop(self):
pass

partialmeth = functools.partialmethod(meth)

async def coroutinemeth(self):
pass

partial_coroutinemeth = functools.partialmethod(coroutinemeth)

@classmethod
async def coroutineclassmeth(cls):
"""A documented coroutine classmethod"""
pass


class Inherited(Base):
pass


def func():
pass


async def coroutinefunc():
pass


async def asyncgenerator():
yield

partial_func = functools.partial(func)
partial_coroutinefunc = functools.partial(coroutinefunc)

builtin_func = print
partial_builtin_func = functools.partial(print)


class Descriptor:
def __get__(self, obj, typ=None):
pass


class _Callable:
def __call__(self):
pass


def _decorator(f):
@functools.wraps(f)
def wrapper():
return f()
return wrapper


def test_TypeAliasForwardRef():
alias = TypeAliasForwardRef('example')
assert stringify_annotation(alias, 'fully-qualified-except-typing') == 'example'
Expand Down Expand Up @@ -619,47 +687,39 @@ class Qux:
inspect.getslots(Bar())


@pytest.mark.sphinx(testroot='ext-autodoc')
def test_isclassmethod(app):
from target.methods import Base, Inherited

def test_isclassmethod():
assert inspect.isclassmethod(Base.classmeth) is True
assert inspect.isclassmethod(Base.meth) is False
assert inspect.isclassmethod(Inherited.classmeth) is True
assert inspect.isclassmethod(Inherited.meth) is False


@pytest.mark.sphinx(testroot='ext-autodoc')
def test_isstaticmethod(app):
from target.methods import Base, Inherited

def test_isstaticmethod():
assert inspect.isstaticmethod(Base.staticmeth, Base, 'staticmeth') is True
assert inspect.isstaticmethod(Base.meth, Base, 'meth') is False
assert inspect.isstaticmethod(Inherited.staticmeth, Inherited, 'staticmeth') is True
assert inspect.isstaticmethod(Inherited.meth, Inherited, 'meth') is False


@pytest.mark.sphinx(testroot='ext-autodoc')
def test_iscoroutinefunction(app):
from target.functions import coroutinefunc, func, partial_coroutinefunc
from target.methods import Base

def test_iscoroutinefunction():
assert inspect.iscoroutinefunction(func) is False # function
assert inspect.iscoroutinefunction(coroutinefunc) is True # coroutine
assert inspect.iscoroutinefunction(partial_coroutinefunc) is True # partial-ed coroutine
assert inspect.iscoroutinefunction(Base.meth) is False # method
assert inspect.iscoroutinefunction(Base.coroutinemeth) is True # coroutine-method
assert inspect.iscoroutinefunction(Base.__dict__["coroutineclassmeth"]) is True # coroutine classmethod

# partial-ed coroutine-method
partial_coroutinemeth = Base.__dict__['partial_coroutinemeth']
assert inspect.iscoroutinefunction(partial_coroutinemeth) is True


@pytest.mark.sphinx(testroot='ext-autodoc')
def test_isfunction(app):
from target.functions import builtin_func, func, partial_builtin_func, partial_func
from target.methods import Base
def test_iscoroutinefunction_wrapped():
# function wrapping a callable obj
assert inspect.isfunction(_decorator(coroutinefunc)) is True


def test_isfunction():
assert inspect.isfunction(func) is True # function
assert inspect.isfunction(partial_func) is True # partial-ed function
assert inspect.isfunction(Base.meth) is True # method of class
Expand All @@ -669,11 +729,12 @@ def test_isfunction(app):
assert inspect.isfunction(partial_builtin_func) is False # partial-ed builtin function


@pytest.mark.sphinx(testroot='ext-autodoc')
def test_isbuiltin(app):
from target.functions import builtin_func, func, partial_builtin_func, partial_func
from target.methods import Base
def test_isfunction_wrapped():
# function wrapping a callable obj
assert inspect.isfunction(_decorator(_Callable())) is True


def test_isbuiltin():
assert inspect.isbuiltin(builtin_func) is True # builtin function
assert inspect.isbuiltin(partial_builtin_func) is True # partial-ed builtin function
assert inspect.isbuiltin(func) is False # function
Expand All @@ -682,26 +743,15 @@ def test_isbuiltin(app):
assert inspect.isbuiltin(Base().meth) is False # method of instance


@pytest.mark.sphinx(testroot='ext-autodoc')
def test_isdescriptor(app):
from target.functions import func
from target.methods import Base

def test_isdescriptor():
assert inspect.isdescriptor(Base.prop) is True # property of class
assert inspect.isdescriptor(Base().prop) is False # property of instance
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

def test_isattributedescriptor():
assert inspect.isattributedescriptor(Base.prop) is True # property
assert inspect.isattributedescriptor(Base.meth) is False # method
assert inspect.isattributedescriptor(Base.staticmeth) is False # staticmethod
Expand All @@ -724,25 +774,28 @@ def __get__(self, obj, typ=None):
pass


@pytest.mark.sphinx(testroot='ext-autodoc')
def test_isproperty(app):
from target.functions import func
from target.methods import Base

def test_isproperty():
assert inspect.isproperty(Base.prop) is True # property of class
assert inspect.isproperty(Base().prop) is False # property of instance
assert inspect.isproperty(Base.meth) is False # method of class
assert inspect.isproperty(Base().meth) is False # method of instance
assert inspect.isproperty(func) is False # function


@pytest.mark.sphinx(testroot='ext-autodoc')
def test_isgenericalias(app):
from target.genericalias import C, T
from target.methods import Base
def test_isgenericalias():
#: A list of int
T = List[int] # NoQA: UP006
S = list[Union[str, None]]

C = Callable[[int], None] # a generic alias not having a doccomment

assert inspect.isgenericalias(C) is True
assert inspect.isgenericalias(Callable) is True
assert inspect.isgenericalias(T) is True
assert inspect.isgenericalias(List) is True # NoQA: UP006
assert inspect.isgenericalias(S) is True
assert inspect.isgenericalias(list) is False
assert inspect.isgenericalias([]) is False
assert inspect.isgenericalias(object()) is False
assert inspect.isgenericalias(Base) is False

Expand Down

0 comments on commit 7cce00a

Please sign in to comment.