834 lines
28 KiB
Python
834 lines
28 KiB
Python
|
"""Helpers for inspecting Python modules."""
|
||
|
|
||
|
from __future__ import annotations
|
||
|
|
||
|
import ast
|
||
|
import builtins
|
||
|
import contextlib
|
||
|
import enum
|
||
|
import inspect
|
||
|
import re
|
||
|
import sys
|
||
|
import types
|
||
|
import typing
|
||
|
from collections.abc import Mapping, Sequence
|
||
|
from functools import cached_property, partial, partialmethod, singledispatchmethod
|
||
|
from importlib import import_module
|
||
|
from inspect import ( # noqa: F401
|
||
|
Parameter,
|
||
|
isasyncgenfunction,
|
||
|
isclass,
|
||
|
ismethod,
|
||
|
ismethoddescriptor,
|
||
|
ismodule,
|
||
|
)
|
||
|
from io import StringIO
|
||
|
from types import (
|
||
|
ClassMethodDescriptorType,
|
||
|
MethodDescriptorType,
|
||
|
MethodType,
|
||
|
ModuleType,
|
||
|
WrapperDescriptorType,
|
||
|
)
|
||
|
from typing import Any, Callable, cast
|
||
|
|
||
|
from sphinx.pycode.ast import unparse as ast_unparse
|
||
|
from sphinx.util import logging
|
||
|
from sphinx.util.typing import ForwardRef, stringify_annotation
|
||
|
|
||
|
logger = logging.getLogger(__name__)
|
||
|
|
||
|
memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE)
|
||
|
|
||
|
|
||
|
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:
|
||
|
return inspect.unwrap(obj)
|
||
|
except ValueError:
|
||
|
# might be a mock object
|
||
|
return obj
|
||
|
|
||
|
|
||
|
def unwrap_all(obj: Any, *, stop: Callable | None = None) -> Any:
|
||
|
"""
|
||
|
Get an original object from wrapped object (unwrapping partials, wrapped
|
||
|
functions, and other decorators).
|
||
|
"""
|
||
|
while True:
|
||
|
if stop and stop(obj):
|
||
|
return obj
|
||
|
if ispartial(obj):
|
||
|
obj = obj.func
|
||
|
elif inspect.isroutine(obj) and hasattr(obj, '__wrapped__'):
|
||
|
obj = obj.__wrapped__
|
||
|
elif isclassmethod(obj) or isstaticmethod(obj):
|
||
|
obj = obj.__func__
|
||
|
else:
|
||
|
return obj
|
||
|
|
||
|
|
||
|
def getall(obj: Any) -> Sequence[str] | None:
|
||
|
"""Get __all__ attribute of the module as dict.
|
||
|
|
||
|
Return None if given *obj* does not have __all__.
|
||
|
Raises ValueError if given *obj* have invalid __all__.
|
||
|
"""
|
||
|
__all__ = safe_getattr(obj, '__all__', None)
|
||
|
if __all__ is None:
|
||
|
return None
|
||
|
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]:
|
||
|
"""Get __annotations__ from given *obj* safely."""
|
||
|
__annotations__ = safe_getattr(obj, '__annotations__', None)
|
||
|
if isinstance(__annotations__, Mapping):
|
||
|
return __annotations__
|
||
|
else:
|
||
|
return {}
|
||
|
|
||
|
|
||
|
def getglobals(obj: Any) -> Mapping[str, Any]:
|
||
|
"""Get __globals__ from given *obj* safely."""
|
||
|
__globals__ = safe_getattr(obj, '__globals__', None)
|
||
|
if isinstance(__globals__, Mapping):
|
||
|
return __globals__
|
||
|
else:
|
||
|
return {}
|
||
|
|
||
|
|
||
|
def getmro(obj: Any) -> tuple[type, ...]:
|
||
|
"""Get __mro__ from given *obj* safely."""
|
||
|
__mro__ = safe_getattr(obj, '__mro__', None)
|
||
|
if isinstance(__mro__, tuple):
|
||
|
return __mro__
|
||
|
else:
|
||
|
return ()
|
||
|
|
||
|
|
||
|
def getorigbases(obj: Any) -> tuple[Any, ...] | None:
|
||
|
"""Get __orig_bases__ from *obj* safely."""
|
||
|
if not inspect.isclass(obj):
|
||
|
return None
|
||
|
|
||
|
# Get __orig_bases__ from obj.__dict__ to avoid accessing the parent's __orig_bases__.
|
||
|
# refs: https://github.com/sphinx-doc/sphinx/issues/9607
|
||
|
__dict__ = safe_getattr(obj, '__dict__', {})
|
||
|
__orig_bases__ = __dict__.get('__orig_bases__')
|
||
|
if isinstance(__orig_bases__, tuple) and len(__orig_bases__) > 0:
|
||
|
return __orig_bases__
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
|
||
|
def getslots(obj: Any) -> dict[str, Any] | None:
|
||
|
"""Get __slots__ attribute of the class as dict.
|
||
|
|
||
|
Return None if gienv *obj* does not have __slots__.
|
||
|
Raises TypeError if given *obj* is not a class.
|
||
|
Raises ValueError if given *obj* have invalid __slots__.
|
||
|
"""
|
||
|
if not inspect.isclass(obj):
|
||
|
raise TypeError
|
||
|
|
||
|
__slots__ = safe_getattr(obj, '__slots__', None)
|
||
|
if __slots__ is None:
|
||
|
return None
|
||
|
elif isinstance(__slots__, dict):
|
||
|
return __slots__
|
||
|
elif isinstance(__slots__, str):
|
||
|
return {__slots__: None}
|
||
|
elif isinstance(__slots__, (list, tuple)):
|
||
|
return dict.fromkeys(__slots__)
|
||
|
else:
|
||
|
raise ValueError
|
||
|
|
||
|
|
||
|
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)
|
||
|
__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:
|
||
|
"""Check if the object is subclass of enum."""
|
||
|
return inspect.isclass(x) and issubclass(x, enum.Enum)
|
||
|
|
||
|
|
||
|
def isenumattribute(x: Any) -> bool:
|
||
|
"""Check if the object is attribute of enum."""
|
||
|
return isinstance(x, enum.Enum)
|
||
|
|
||
|
|
||
|
def unpartial(obj: Any) -> Any:
|
||
|
"""Get an original object from partial object.
|
||
|
|
||
|
This returns given object itself if not partial.
|
||
|
"""
|
||
|
while ispartial(obj):
|
||
|
obj = obj.func
|
||
|
|
||
|
return obj
|
||
|
|
||
|
|
||
|
def ispartial(obj: Any) -> bool:
|
||
|
"""Check if the object is partial."""
|
||
|
return isinstance(obj, (partial, partialmethod))
|
||
|
|
||
|
|
||
|
def isclassmethod(obj: Any, cls: Any = None, name: str | None = None) -> bool:
|
||
|
"""Check if the object is classmethod."""
|
||
|
if isinstance(obj, classmethod):
|
||
|
return True
|
||
|
if inspect.ismethod(obj) and obj.__self__ is not None and isclass(obj.__self__):
|
||
|
return True
|
||
|
if cls and name:
|
||
|
placeholder = object()
|
||
|
for basecls in getmro(cls):
|
||
|
meth = basecls.__dict__.get(name, placeholder)
|
||
|
if meth is not placeholder:
|
||
|
return isclassmethod(meth)
|
||
|
|
||
|
return False
|
||
|
|
||
|
|
||
|
def isstaticmethod(obj: Any, cls: Any = None, name: str | None = None) -> bool:
|
||
|
"""Check if the object is staticmethod."""
|
||
|
if isinstance(obj, staticmethod):
|
||
|
return True
|
||
|
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
|
||
|
|
||
|
|
||
|
def isdescriptor(x: Any) -> bool:
|
||
|
"""Check if the object is some kind of descriptor."""
|
||
|
return any(
|
||
|
callable(safe_getattr(x, item, None))
|
||
|
for item in ['__get__', '__set__', '__delete__']
|
||
|
)
|
||
|
|
||
|
|
||
|
def isabstractmethod(obj: Any) -> bool:
|
||
|
"""Check if the object is an abstractmethod."""
|
||
|
return safe_getattr(obj, '__isabstractmethod__', False) is True
|
||
|
|
||
|
|
||
|
def isboundmethod(method: MethodType) -> bool:
|
||
|
"""Check if the method is a bound method."""
|
||
|
return safe_getattr(method, '__self__', None) is not None
|
||
|
|
||
|
|
||
|
def is_cython_function_or_method(obj: Any) -> bool:
|
||
|
"""Check if the object is a function or method in cython."""
|
||
|
try:
|
||
|
return obj.__class__.__name__ == 'cython_function_or_method'
|
||
|
except AttributeError:
|
||
|
return False
|
||
|
|
||
|
|
||
|
def isattributedescriptor(obj: Any) -> bool:
|
||
|
"""Check if the object is an attribute like descriptor."""
|
||
|
if inspect.isdatadescriptor(obj):
|
||
|
# data descriptor is kind of attribute
|
||
|
return True
|
||
|
if isdescriptor(obj):
|
||
|
# non data descriptor
|
||
|
unwrapped = unwrap(obj)
|
||
|
if isfunction(unwrapped) or isbuiltin(unwrapped) or inspect.ismethod(unwrapped):
|
||
|
# attribute must not be either function, builtin and method
|
||
|
return False
|
||
|
if is_cython_function_or_method(unwrapped):
|
||
|
# attribute must not be either function and method (for cython)
|
||
|
return False
|
||
|
if inspect.isclass(unwrapped):
|
||
|
# attribute must not be a class
|
||
|
return False
|
||
|
if isinstance(unwrapped, (ClassMethodDescriptorType,
|
||
|
MethodDescriptorType,
|
||
|
WrapperDescriptorType)):
|
||
|
# attribute must not be a method descriptor
|
||
|
return False
|
||
|
if type(unwrapped).__name__ == "instancemethod":
|
||
|
# attribute must not be an instancemethod (C-API)
|
||
|
return False
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
|
||
|
def is_singledispatch_function(obj: Any) -> bool:
|
||
|
"""Check if the object is singledispatch function."""
|
||
|
return (inspect.isfunction(obj) and
|
||
|
hasattr(obj, 'dispatch') and
|
||
|
hasattr(obj, 'register') and
|
||
|
obj.dispatch.__module__ == 'functools')
|
||
|
|
||
|
|
||
|
def is_singledispatch_method(obj: Any) -> bool:
|
||
|
"""Check if the object is singledispatch method."""
|
||
|
return isinstance(obj, singledispatchmethod)
|
||
|
|
||
|
|
||
|
def isfunction(obj: Any) -> bool:
|
||
|
"""Check if the object is function."""
|
||
|
return inspect.isfunction(unpartial(obj))
|
||
|
|
||
|
|
||
|
def isbuiltin(obj: Any) -> bool:
|
||
|
"""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(unpartial(obj))
|
||
|
|
||
|
|
||
|
def iscoroutinefunction(obj: Any) -> bool:
|
||
|
"""Check if the object is coroutine-function."""
|
||
|
def iswrappedcoroutine(obj: Any) -> bool:
|
||
|
"""Check if the object is wrapped coroutine-function."""
|
||
|
if isstaticmethod(obj) or isclassmethod(obj) or ispartial(obj):
|
||
|
# staticmethod, classmethod and partial method are not a wrapped coroutine-function
|
||
|
# Note: Since 3.10, staticmethod and classmethod becomes a kind of wrappers
|
||
|
return False
|
||
|
return hasattr(obj, '__wrapped__')
|
||
|
|
||
|
obj = unwrap_all(obj, stop=iswrappedcoroutine)
|
||
|
return inspect.iscoroutinefunction(obj)
|
||
|
|
||
|
|
||
|
def isproperty(obj: Any) -> bool:
|
||
|
"""Check if the object is property."""
|
||
|
return isinstance(obj, (property, cached_property))
|
||
|
|
||
|
|
||
|
def isgenericalias(obj: Any) -> bool:
|
||
|
"""Check if the object is GenericAlias."""
|
||
|
return isinstance(
|
||
|
obj, (types.GenericAlias, typing._BaseGenericAlias)) # type: ignore[attr-defined]
|
||
|
|
||
|
|
||
|
def safe_getattr(obj: Any, name: str, *defargs: Any) -> Any:
|
||
|
"""A getattr() that turns all exceptions into AttributeErrors."""
|
||
|
try:
|
||
|
return getattr(obj, name, *defargs)
|
||
|
except Exception as exc:
|
||
|
# sometimes accessing a property raises an exception (e.g.
|
||
|
# NotImplementedError), so let's try to read the attribute directly
|
||
|
try:
|
||
|
# In case the object does weird things with attribute access
|
||
|
# such that accessing `obj.__dict__` may raise an exception
|
||
|
return obj.__dict__[name]
|
||
|
except Exception:
|
||
|
pass
|
||
|
|
||
|
# this is a catch-all for all the weird things that some modules do
|
||
|
# with attribute access
|
||
|
if defargs:
|
||
|
return defargs[0]
|
||
|
|
||
|
raise AttributeError(name) from exc
|
||
|
|
||
|
|
||
|
def object_description(obj: Any, *, _seen: frozenset = frozenset()) -> str:
|
||
|
"""A repr() implementation that returns text safe to use in reST context.
|
||
|
|
||
|
Maintains a set of 'seen' object IDs to detect and avoid infinite recursion.
|
||
|
"""
|
||
|
seen = _seen
|
||
|
if isinstance(obj, dict):
|
||
|
if id(obj) in seen:
|
||
|
return 'dict(...)'
|
||
|
seen |= {id(obj)}
|
||
|
try:
|
||
|
sorted_keys = sorted(obj)
|
||
|
except TypeError:
|
||
|
# Cannot sort dict keys, fall back to using descriptions as a sort key
|
||
|
sorted_keys = sorted(obj, key=lambda k: object_description(k, _seen=seen))
|
||
|
|
||
|
items = ((object_description(key, _seen=seen),
|
||
|
object_description(obj[key], _seen=seen)) for key in sorted_keys)
|
||
|
return '{%s}' % ', '.join(f'{key}: {value}' for (key, value) in items)
|
||
|
elif isinstance(obj, set):
|
||
|
if id(obj) in seen:
|
||
|
return 'set(...)'
|
||
|
seen |= {id(obj)}
|
||
|
try:
|
||
|
sorted_values = sorted(obj)
|
||
|
except TypeError:
|
||
|
# Cannot sort set values, fall back to using descriptions as a sort key
|
||
|
sorted_values = sorted(obj, key=lambda x: object_description(x, _seen=seen))
|
||
|
return '{%s}' % ', '.join(object_description(x, _seen=seen) for x in sorted_values)
|
||
|
elif isinstance(obj, frozenset):
|
||
|
if id(obj) in seen:
|
||
|
return 'frozenset(...)'
|
||
|
seen |= {id(obj)}
|
||
|
try:
|
||
|
sorted_values = sorted(obj)
|
||
|
except TypeError:
|
||
|
# Cannot sort frozenset values, fall back to using descriptions as a sort key
|
||
|
sorted_values = sorted(obj, key=lambda x: object_description(x, _seen=seen))
|
||
|
return 'frozenset({%s})' % ', '.join(object_description(x, _seen=seen)
|
||
|
for x in sorted_values)
|
||
|
elif isinstance(obj, enum.Enum):
|
||
|
return f'{obj.__class__.__name__}.{obj.name}'
|
||
|
elif isinstance(obj, tuple):
|
||
|
if id(obj) in seen:
|
||
|
return 'tuple(...)'
|
||
|
seen |= frozenset([id(obj)])
|
||
|
return '(%s%s)' % (
|
||
|
', '.join(object_description(x, _seen=seen) for x in obj),
|
||
|
',' * (len(obj) == 1),
|
||
|
)
|
||
|
elif isinstance(obj, list):
|
||
|
if id(obj) in seen:
|
||
|
return 'list(...)'
|
||
|
seen |= {id(obj)}
|
||
|
return '[%s]' % ', '.join(object_description(x, _seen=seen) for x in obj)
|
||
|
|
||
|
try:
|
||
|
s = repr(obj)
|
||
|
except Exception as exc:
|
||
|
raise ValueError from exc
|
||
|
# Strip non-deterministic memory addresses such as
|
||
|
# ``<__main__.A at 0x7f68cb685710>``
|
||
|
s = memory_address_re.sub('', s)
|
||
|
return s.replace('\n', ' ')
|
||
|
|
||
|
|
||
|
def is_builtin_class_method(obj: Any, attr_name: str) -> bool:
|
||
|
"""If attr_name is implemented at builtin class, return True.
|
||
|
|
||
|
>>> is_builtin_class_method(int, '__init__')
|
||
|
True
|
||
|
|
||
|
Why this function needed? CPython implements int.__init__ by Descriptor
|
||
|
but PyPy implements it by pure Python code.
|
||
|
"""
|
||
|
try:
|
||
|
mro = getmro(obj)
|
||
|
cls = next(c for c in mro if attr_name in safe_getattr(c, '__dict__', {}))
|
||
|
except StopIteration:
|
||
|
return False
|
||
|
|
||
|
try:
|
||
|
name = safe_getattr(cls, '__name__')
|
||
|
except AttributeError:
|
||
|
return False
|
||
|
|
||
|
return getattr(builtins, name, None) is cls
|
||
|
|
||
|
|
||
|
class DefaultValue:
|
||
|
"""A simple wrapper for default value of the parameters of overload functions."""
|
||
|
|
||
|
def __init__(self, value: str) -> None:
|
||
|
self.value = value
|
||
|
|
||
|
def __eq__(self, other: object) -> bool:
|
||
|
return self.value == other
|
||
|
|
||
|
def __repr__(self) -> str:
|
||
|
return self.value
|
||
|
|
||
|
|
||
|
class TypeAliasForwardRef:
|
||
|
"""Pseudo typing class for autodoc_type_aliases.
|
||
|
|
||
|
This avoids the error on evaluating the type inside `get_type_hints()`.
|
||
|
"""
|
||
|
def __init__(self, name: str) -> None:
|
||
|
self.name = name
|
||
|
|
||
|
def __call__(self) -> None:
|
||
|
# Dummy method to imitate special typing classes
|
||
|
pass
|
||
|
|
||
|
def __eq__(self, other: Any) -> bool:
|
||
|
return self.name == other
|
||
|
|
||
|
def __hash__(self) -> int:
|
||
|
return hash(self.name)
|
||
|
|
||
|
def __repr__(self) -> str:
|
||
|
return self.name
|
||
|
|
||
|
|
||
|
class TypeAliasModule:
|
||
|
"""Pseudo module class for autodoc_type_aliases."""
|
||
|
|
||
|
def __init__(self, modname: str, mapping: dict[str, str]) -> None:
|
||
|
self.__modname = modname
|
||
|
self.__mapping = mapping
|
||
|
|
||
|
self.__module: ModuleType | None = None
|
||
|
|
||
|
def __getattr__(self, name: str) -> Any:
|
||
|
fullname = '.'.join(filter(None, [self.__modname, name]))
|
||
|
if fullname in self.__mapping:
|
||
|
# exactly matched
|
||
|
return TypeAliasForwardRef(self.__mapping[fullname])
|
||
|
else:
|
||
|
prefix = fullname + '.'
|
||
|
nested = {k: v for k, v in self.__mapping.items() if k.startswith(prefix)}
|
||
|
if nested:
|
||
|
# sub modules or classes found
|
||
|
return TypeAliasModule(fullname, nested)
|
||
|
else:
|
||
|
# no sub modules or classes found.
|
||
|
try:
|
||
|
# return the real submodule if exists
|
||
|
return import_module(fullname)
|
||
|
except ImportError:
|
||
|
# return the real class
|
||
|
if self.__module is None:
|
||
|
self.__module = import_module(self.__modname)
|
||
|
|
||
|
return getattr(self.__module, name)
|
||
|
|
||
|
|
||
|
class TypeAliasNamespace(dict[str, Any]):
|
||
|
"""Pseudo namespace class for autodoc_type_aliases.
|
||
|
|
||
|
This enables to look up nested modules and classes like `mod1.mod2.Class`.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, mapping: dict[str, str]) -> None:
|
||
|
self.__mapping = mapping
|
||
|
|
||
|
def __getitem__(self, key: str) -> Any:
|
||
|
if key in self.__mapping:
|
||
|
# exactly matched
|
||
|
return TypeAliasForwardRef(self.__mapping[key])
|
||
|
else:
|
||
|
prefix = key + '.'
|
||
|
nested = {k: v for k, v in self.__mapping.items() if k.startswith(prefix)}
|
||
|
if nested:
|
||
|
# sub modules or classes found
|
||
|
return TypeAliasModule(key, nested)
|
||
|
else:
|
||
|
raise KeyError
|
||
|
|
||
|
|
||
|
def _should_unwrap(subject: Callable) -> bool:
|
||
|
"""Check the function should be unwrapped on getting signature."""
|
||
|
__globals__ = getglobals(subject)
|
||
|
if (__globals__.get('__name__') == 'contextlib' and
|
||
|
__globals__.get('__file__') == contextlib.__file__):
|
||
|
# contextmanger should be unwrapped
|
||
|
return True
|
||
|
|
||
|
return False
|
||
|
|
||
|
|
||
|
def signature(subject: Callable, bound_method: bool = False, type_aliases: dict | None = None,
|
||
|
) -> inspect.Signature:
|
||
|
"""Return a Signature object for the given *subject*.
|
||
|
|
||
|
:param bound_method: Specify *subject* is a bound method or not
|
||
|
"""
|
||
|
if type_aliases is None:
|
||
|
type_aliases = {}
|
||
|
|
||
|
try:
|
||
|
if _should_unwrap(subject):
|
||
|
signature = inspect.signature(subject)
|
||
|
else:
|
||
|
signature = inspect.signature(subject, follow_wrapped=True)
|
||
|
except ValueError:
|
||
|
# follow built-in wrappers up (ex. functools.lru_cache)
|
||
|
signature = inspect.signature(subject)
|
||
|
parameters = list(signature.parameters.values())
|
||
|
return_annotation = signature.return_annotation
|
||
|
|
||
|
try:
|
||
|
# Resolve annotations using ``get_type_hints()`` and type_aliases.
|
||
|
localns = TypeAliasNamespace(type_aliases)
|
||
|
annotations = typing.get_type_hints(subject, None, localns)
|
||
|
for i, param in enumerate(parameters):
|
||
|
if param.name in annotations:
|
||
|
annotation = annotations[param.name]
|
||
|
if isinstance(annotation, TypeAliasForwardRef):
|
||
|
annotation = annotation.name
|
||
|
parameters[i] = param.replace(annotation=annotation)
|
||
|
if 'return' in annotations:
|
||
|
if isinstance(annotations['return'], TypeAliasForwardRef):
|
||
|
return_annotation = annotations['return'].name
|
||
|
else:
|
||
|
return_annotation = annotations['return']
|
||
|
except Exception:
|
||
|
# ``get_type_hints()`` does not support some kind of objects like partial,
|
||
|
# ForwardRef and so on.
|
||
|
pass
|
||
|
|
||
|
if bound_method:
|
||
|
if inspect.ismethod(subject):
|
||
|
# ``inspect.signature()`` considers the subject is a bound method and removes
|
||
|
# first argument from signature. Therefore no skips are needed here.
|
||
|
pass
|
||
|
else:
|
||
|
if len(parameters) > 0:
|
||
|
parameters.pop(0)
|
||
|
|
||
|
# To allow to create signature object correctly for pure python functions,
|
||
|
# pass an internal parameter __validate_parameters__=False to Signature
|
||
|
#
|
||
|
# For example, this helps a function having a default value `inspect._empty`.
|
||
|
# refs: https://github.com/sphinx-doc/sphinx/issues/7935
|
||
|
return inspect.Signature(parameters, return_annotation=return_annotation,
|
||
|
__validate_parameters__=False)
|
||
|
|
||
|
|
||
|
def evaluate_signature(sig: inspect.Signature, globalns: dict | None = None,
|
||
|
localns: dict | None = None,
|
||
|
) -> inspect.Signature:
|
||
|
"""Evaluate unresolved type annotations in a signature object."""
|
||
|
def evaluate_forwardref(ref: ForwardRef, globalns: dict, localns: dict) -> Any:
|
||
|
"""Evaluate a forward reference."""
|
||
|
return ref._evaluate(globalns, localns, frozenset())
|
||
|
|
||
|
def evaluate(annotation: Any, globalns: dict, localns: dict) -> Any:
|
||
|
"""Evaluate unresolved type annotation."""
|
||
|
try:
|
||
|
if isinstance(annotation, str):
|
||
|
ref = ForwardRef(annotation, True)
|
||
|
annotation = evaluate_forwardref(ref, globalns, localns)
|
||
|
|
||
|
if isinstance(annotation, ForwardRef):
|
||
|
annotation = evaluate_forwardref(ref, globalns, localns)
|
||
|
elif isinstance(annotation, str):
|
||
|
# might be a ForwardRef'ed annotation in overloaded functions
|
||
|
ref = ForwardRef(annotation, True)
|
||
|
annotation = evaluate_forwardref(ref, globalns, localns)
|
||
|
except (NameError, TypeError):
|
||
|
# failed to evaluate type. skipped.
|
||
|
pass
|
||
|
|
||
|
return annotation
|
||
|
|
||
|
if globalns is None:
|
||
|
globalns = {}
|
||
|
if localns is None:
|
||
|
localns = globalns
|
||
|
|
||
|
parameters = list(sig.parameters.values())
|
||
|
for i, param in enumerate(parameters):
|
||
|
if param.annotation:
|
||
|
annotation = evaluate(param.annotation, globalns, localns)
|
||
|
parameters[i] = param.replace(annotation=annotation)
|
||
|
|
||
|
return_annotation = sig.return_annotation
|
||
|
if return_annotation:
|
||
|
return_annotation = evaluate(return_annotation, globalns, localns)
|
||
|
|
||
|
return sig.replace(parameters=parameters, return_annotation=return_annotation)
|
||
|
|
||
|
|
||
|
def stringify_signature(sig: inspect.Signature, show_annotation: bool = True,
|
||
|
show_return_annotation: bool = True,
|
||
|
unqualified_typehints: bool = False) -> str:
|
||
|
"""Stringify a Signature object.
|
||
|
|
||
|
:param show_annotation: If enabled, show annotations on the signature
|
||
|
:param show_return_annotation: If enabled, show annotation of the return value
|
||
|
:param unqualified_typehints: If enabled, show annotations as unqualified
|
||
|
(ex. io.StringIO -> StringIO)
|
||
|
"""
|
||
|
if unqualified_typehints:
|
||
|
mode = 'smart'
|
||
|
else:
|
||
|
mode = 'fully-qualified'
|
||
|
|
||
|
args = []
|
||
|
last_kind = None
|
||
|
for param in sig.parameters.values():
|
||
|
if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY:
|
||
|
# PEP-570: Separator for Positional Only Parameter: /
|
||
|
args.append('/')
|
||
|
if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD,
|
||
|
param.POSITIONAL_ONLY,
|
||
|
None):
|
||
|
# PEP-3102: Separator for Keyword Only Parameter: *
|
||
|
args.append('*')
|
||
|
|
||
|
arg = StringIO()
|
||
|
if param.kind == param.VAR_POSITIONAL:
|
||
|
arg.write('*' + param.name)
|
||
|
elif param.kind == param.VAR_KEYWORD:
|
||
|
arg.write('**' + param.name)
|
||
|
else:
|
||
|
arg.write(param.name)
|
||
|
|
||
|
if show_annotation and param.annotation is not param.empty:
|
||
|
arg.write(': ')
|
||
|
arg.write(stringify_annotation(param.annotation, mode))
|
||
|
if param.default is not param.empty:
|
||
|
if show_annotation and param.annotation is not param.empty:
|
||
|
arg.write(' = ')
|
||
|
else:
|
||
|
arg.write('=')
|
||
|
arg.write(object_description(param.default))
|
||
|
|
||
|
args.append(arg.getvalue())
|
||
|
last_kind = param.kind
|
||
|
|
||
|
if last_kind == Parameter.POSITIONAL_ONLY:
|
||
|
# PEP-570: Separator for Positional Only Parameter: /
|
||
|
args.append('/')
|
||
|
|
||
|
concatenated_args = ', '.join(args)
|
||
|
if (sig.return_annotation is Parameter.empty or
|
||
|
show_annotation is False or
|
||
|
show_return_annotation is False):
|
||
|
return f'({concatenated_args})'
|
||
|
else:
|
||
|
annotation = stringify_annotation(sig.return_annotation, mode)
|
||
|
return f'({concatenated_args}) -> {annotation}'
|
||
|
|
||
|
|
||
|
def signature_from_str(signature: str) -> inspect.Signature:
|
||
|
"""Create a Signature object from string."""
|
||
|
code = 'def func' + signature + ': pass'
|
||
|
module = ast.parse(code)
|
||
|
function = cast(ast.FunctionDef, module.body[0])
|
||
|
|
||
|
return signature_from_ast(function, code)
|
||
|
|
||
|
|
||
|
def signature_from_ast(node: ast.FunctionDef, code: str = '') -> inspect.Signature:
|
||
|
"""Create a Signature object from AST *node*."""
|
||
|
args = node.args
|
||
|
defaults = list(args.defaults)
|
||
|
params = []
|
||
|
if hasattr(args, "posonlyargs"):
|
||
|
posonlyargs = len(args.posonlyargs)
|
||
|
positionals = posonlyargs + len(args.args)
|
||
|
else:
|
||
|
posonlyargs = 0
|
||
|
positionals = len(args.args)
|
||
|
|
||
|
for _ in range(len(defaults), positionals):
|
||
|
defaults.insert(0, Parameter.empty) # type: ignore[arg-type]
|
||
|
|
||
|
if hasattr(args, "posonlyargs"):
|
||
|
for i, arg in enumerate(args.posonlyargs):
|
||
|
if defaults[i] is Parameter.empty:
|
||
|
default = Parameter.empty
|
||
|
else:
|
||
|
default = DefaultValue(
|
||
|
ast_unparse(defaults[i], code)) # type: ignore[assignment]
|
||
|
|
||
|
annotation = ast_unparse(arg.annotation, code) or Parameter.empty
|
||
|
params.append(Parameter(arg.arg, Parameter.POSITIONAL_ONLY,
|
||
|
default=default, annotation=annotation))
|
||
|
|
||
|
for i, arg in enumerate(args.args):
|
||
|
if defaults[i + posonlyargs] is Parameter.empty:
|
||
|
default = Parameter.empty
|
||
|
else:
|
||
|
default = DefaultValue(
|
||
|
ast_unparse(defaults[i + posonlyargs], code), # type: ignore[assignment]
|
||
|
)
|
||
|
|
||
|
annotation = ast_unparse(arg.annotation, code) or Parameter.empty
|
||
|
params.append(Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD,
|
||
|
default=default, annotation=annotation))
|
||
|
|
||
|
if args.vararg:
|
||
|
annotation = ast_unparse(args.vararg.annotation, code) or Parameter.empty
|
||
|
params.append(Parameter(args.vararg.arg, Parameter.VAR_POSITIONAL,
|
||
|
annotation=annotation))
|
||
|
|
||
|
for i, arg in enumerate(args.kwonlyargs):
|
||
|
if args.kw_defaults[i] is None:
|
||
|
default = Parameter.empty
|
||
|
else:
|
||
|
default = DefaultValue(
|
||
|
ast_unparse(args.kw_defaults[i], code)) # type: ignore[arg-type,assignment]
|
||
|
annotation = ast_unparse(arg.annotation, code) or Parameter.empty
|
||
|
params.append(Parameter(arg.arg, Parameter.KEYWORD_ONLY, default=default,
|
||
|
annotation=annotation))
|
||
|
|
||
|
if args.kwarg:
|
||
|
annotation = ast_unparse(args.kwarg.annotation, code) or Parameter.empty
|
||
|
params.append(Parameter(args.kwarg.arg, Parameter.VAR_KEYWORD,
|
||
|
annotation=annotation))
|
||
|
|
||
|
return_annotation = ast_unparse(node.returns, code) or Parameter.empty
|
||
|
|
||
|
return inspect.Signature(params, return_annotation=return_annotation)
|
||
|
|
||
|
|
||
|
def getdoc(
|
||
|
obj: Any,
|
||
|
attrgetter: Callable = safe_getattr,
|
||
|
allow_inherited: bool = False,
|
||
|
cls: Any = None,
|
||
|
name: str | None = None,
|
||
|
) -> str | None:
|
||
|
"""Get the docstring for the object.
|
||
|
|
||
|
This tries to obtain the docstring for some kind of objects additionally:
|
||
|
|
||
|
* partial functions
|
||
|
* inherited docstring
|
||
|
* inherited decorated methods
|
||
|
"""
|
||
|
def getdoc_internal(obj: Any, attrgetter: Callable = safe_getattr) -> str | None:
|
||
|
doc = attrgetter(obj, '__doc__', None)
|
||
|
if isinstance(doc, str):
|
||
|
return doc
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
if cls and name and isclassmethod(obj, cls, name):
|
||
|
for basecls in getmro(cls):
|
||
|
meth = basecls.__dict__.get(name)
|
||
|
if meth and hasattr(meth, '__func__'):
|
||
|
doc: str | None = getdoc(meth.__func__)
|
||
|
if doc is not None or not allow_inherited:
|
||
|
return doc
|
||
|
|
||
|
doc = getdoc_internal(obj)
|
||
|
if ispartial(obj) and doc == obj.__class__.__doc__:
|
||
|
return getdoc(obj.func)
|
||
|
elif doc is None and allow_inherited:
|
||
|
if cls and name:
|
||
|
# Check a docstring of the attribute or method from super classes.
|
||
|
for basecls in getmro(cls):
|
||
|
meth = safe_getattr(basecls, name, None)
|
||
|
if meth is not None:
|
||
|
doc = getdoc_internal(meth)
|
||
|
if doc is not None:
|
||
|
break
|
||
|
|
||
|
if doc is None:
|
||
|
# retry using `inspect.getdoc()`
|
||
|
for basecls in getmro(cls):
|
||
|
meth = safe_getattr(basecls, name, None)
|
||
|
if meth is not None:
|
||
|
doc = inspect.getdoc(meth)
|
||
|
if doc is not None:
|
||
|
break
|
||
|
|
||
|
if doc is None:
|
||
|
doc = inspect.getdoc(obj)
|
||
|
|
||
|
return doc
|