summaryrefslogtreecommitdiffstats
path: root/sphinx/util/inspect.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/util/inspect.py')
-rw-r--r--sphinx/util/inspect.py833
1 files changed, 833 insertions, 0 deletions
diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py
new file mode 100644
index 0000000..7d7fbb8
--- /dev/null
+++ b/sphinx/util/inspect.py
@@ -0,0 +1,833 @@
+"""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