From 7cce00aa7df39057450df332a5d02dbc2123b2cb Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Fri, 28 Jul 2023 02:05:40 +0100 Subject: [PATCH] Refactor ``sphinx.util.inspect`` and tests (#11527) --- sphinx/util/inspect.py | 35 ++++----- tests/test_util_inspect.py | 141 +++++++++++++++++++++++++------------ 2 files changed, 112 insertions(+), 64 deletions(-) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 8fc6514068a..11b07df83b5 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -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 @@ -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]: @@ -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..new_type' + __module__ = safe_getattr(obj, '__module__', None) + __qualname__ = safe_getattr(obj, '__qualname__', None) + return __module__ == 'typing' and __qualname__ == 'NewType..new_type' def isenumclass(x: Any) -> bool: @@ -209,7 +205,7 @@ 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. @@ -217,7 +213,6 @@ def isstaticmethod(obj: Any, cls: Any = None, name: str | None = None) -> bool: meth = basecls.__dict__.get(name) if meth: return isinstance(meth, staticmethod) - return False @@ -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: diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 70201862add..09be665736f 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -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 @@ -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' @@ -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 @@ -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 @@ -682,11 +743,7 @@ 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 @@ -694,14 +751,7 @@ def test_isdescriptor(app): 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 @@ -724,11 +774,7 @@ 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 @@ -736,13 +782,20 @@ def test_isproperty(app): 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