"""Helpers for inspecting Python modules."""

import builtins
import contextlib
import enum
import inspect
import re
import sys
import types
import typing
from functools import partial, partialmethod
from importlib import import_module
from inspect import Parameter, isclass, ismethod, ismethoddescriptor, ismodule  # NOQA
from io import StringIO
from types import MethodType, ModuleType
from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Tuple, Type, cast

from sphinx.pycode.ast import ast  # for py36-37
from sphinx.pycode.ast import unparse as ast_unparse
from sphinx.util import logging
from sphinx.util.typing import ForwardRef
from sphinx.util.typing import stringify as stringify_annotation

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__)

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)."""
    try:
        if hasattr(obj, '__sphinx_mock__'):
            # Skip unwrapping mock object to avoid RecursionError
            return obj
        else:
            return inspect.unwrap(obj)
    except ValueError:
        # might be a mock object
        return obj


def unwrap_all(obj: Any, *, stop: Optional[Callable] = 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
        elif ispartial(obj):
            obj = obj.func
        elif inspect.isroutine(obj) and hasattr(obj, '__wrapped__'):
            obj = obj.__wrapped__  # type: ignore
        elif isclassmethod(obj):
            obj = obj.__func__
        elif isstaticmethod(obj):
            obj = obj.__func__
        else:
            return obj


def getall(obj: Any) -> Optional[Sequence[str]]:
    """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
    else:
        if (isinstance(__all__, (list, tuple)) and all(isinstance(e, str) for e in __all__)):
            return __all__
        else:
            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) -> Optional[Tuple[Any, ...]]:
    """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) -> Optional[Dict]:
    """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 {e: None for e in __slots__}
    else:
        raise ValueError


def isNewType(obj: Any) -> bool:
    """Check the if object is a kind of NewType."""
    if sys.version_info >= (3, 10):
        return isinstance(obj, typing.NewType)
    else:
        __module__ = safe_getattr(obj, '__module__', None)
        __qualname__ = safe_getattr(obj, '__qualname__', None)
        if __module__ == 'typing' and __qualname__ == 'NewType.<locals>.new_type':
            return True
        else:
            return False


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: Optional[str] = None) -> bool:
    """Check if the object is classmethod."""
    if isinstance(obj, classmethod):
        return True
    elif inspect.ismethod(obj) and obj.__self__ is not None and isclass(obj.__self__):
        return True
    elif 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: Optional[str] = None) -> bool:
    """Check if the object is staticmethod."""
    if isinstance(obj, staticmethod):
        return True
    elif 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:
                if isinstance(meth, staticmethod):
                    return True
                else:
                    return False

    return False


def isdescriptor(x: Any) -> bool:
    """Check if the object is some kind of descriptor."""
    for item in '__get__', '__set__', '__delete__':
        if callable(safe_getattr(x, item, None)):
            return True
    return False


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
    elif 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
        elif is_cython_function_or_method(unwrapped):
            # attribute must not be either function and method (for cython)
            return False
        elif inspect.isclass(unwrapped):
            # attribute must not be a class
            return False
        elif isinstance(unwrapped, (ClassMethodDescriptorType,
                                    MethodDescriptorType,
                                    WrapperDescriptorType)):
            # attribute must not be a method descriptor
            return False
        elif type(unwrapped).__name__ == "instancemethod":
            # attribute must not be an instancemethod (C-API)
            return False
        else:
            return True
    else:
        return False


def is_singledispatch_function(obj: Any) -> bool:
    """Check if the object is singledispatch function."""
    if (inspect.isfunction(obj) and
            hasattr(obj, 'dispatch') and
            hasattr(obj, 'register') and
            obj.dispatch.__module__ == 'functools'):  # type: ignore
        return True
    else:
        return False


def is_singledispatch_method(obj: Any) -> bool:
    """Check if the object is singledispatch method."""
    try:
        from functools import singledispatchmethod  # type: ignore
        return isinstance(obj, singledispatchmethod)
    except ImportError:  # py36-37
        return False


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


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


def isroutine(obj: Any) -> bool:
    """Check is any kind of function or method."""
    return inspect.isroutine(unwrap_all(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
        elif hasattr(obj, '__wrapped__'):
            return True
        else:
            return False

    obj = unwrap_all(obj, stop=iswrappedcoroutine)
    if hasattr(obj, '__code__') and inspect.iscoroutinefunction(obj):
        # check obj.__code__ because iscoroutinefunction() crashes for custom method-like
        # objects (see https://github.com/sphinx-doc/sphinx/issues/6605)
        return True
    else:
        return False


def isasyncgenfunction(obj: Any) -> bool:
    """Check if the object is async-gen function."""
    if hasattr(obj, '__code__') and inspect.isasyncgenfunction(obj):
        # check obj.__code__ because isasyncgenfunction() crashes for custom method-like
        # objects on python3.7 (see https://github.com/sphinx-doc/sphinx/issues/9838)
        return True
    else:
        return False


def isproperty(obj: Any) -> bool:
    """Check if the object is property."""
    if sys.version_info >= (3, 8):
        from functools import cached_property  # cached_property is available since py3.8
        if isinstance(obj, cached_property):
            return True

    return isinstance(obj, property)


def isgenericalias(obj: Any) -> bool:
    """Check if the object is GenericAlias."""
    if (hasattr(typing, '_GenericAlias') and  # only for py37+
            isinstance(obj, typing._GenericAlias)):  # type: ignore
        return True
    elif (hasattr(types, 'GenericAlias') and  # only for py39+
          isinstance(obj, types.GenericAlias)):  # type: ignore
        return True
    elif (hasattr(typing, '_SpecialGenericAlias') and  # for py39+
            isinstance(obj, typing._SpecialGenericAlias)):  # type: ignore
        return True
    else:
        return False


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(object: Any) -> str:
    """A repr() implementation that returns text safe to use in reST context."""
    if isinstance(object, dict):
        try:
            sorted_keys = sorted(object)
        except Exception:
            pass  # Cannot sort dict keys, fall back to generic repr
        else:
            items = ("%s: %s" %
                     (object_description(key), object_description(object[key]))
                     for key in sorted_keys)
            return "{%s}" % ", ".join(items)
    elif isinstance(object, set):
        try:
            sorted_values = sorted(object)
        except TypeError:
            pass  # Cannot sort set values, fall back to generic repr
        else:
            return "{%s}" % ", ".join(object_description(x) for x in sorted_values)
    elif isinstance(object, frozenset):
        try:
            sorted_values = sorted(object)
        except TypeError:
            pass  # Cannot sort frozenset values, fall back to generic repr
        else:
            return "frozenset({%s})" % ", ".join(object_description(x)
                                                 for x in sorted_values)
    elif isinstance(object, enum.Enum):
        return "%s.%s" % (object.__class__.__name__, object.name)

    try:
        s = repr(object)
    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: Optional[ModuleType] = 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 = {}
              ) -> inspect.Signature:
    """Return a Signature object for the given *subject*.

    :param bound_method: Specify *subject* is a bound method or not
    """

    try:
        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
    except IndexError:
        # Until python 3.6.4, cpython has been crashed on inspection for
        # partialmethods not having any arguments.
        # https://bugs.python.org/issue33009
        if hasattr(subject, '_partialmethod'):
            parameters = []
            return_annotation = Parameter.empty
        else:
            raise

    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: Optional[Dict] = None,
                       localns: Optional[Dict] = 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."""
        if sys.version_info > (3, 9):
            return ref._evaluate(globalns, localns, frozenset())
        else:
            return ref._evaluate(globalns, localns)

    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('/')

    if (sig.return_annotation is Parameter.empty or
            show_annotation is False or
            show_return_annotation is False):
        return '(%s)' % ', '.join(args)
    else:
        annotation = stringify_annotation(sig.return_annotation, mode)
        return '(%s) -> %s' % (', '.join(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])  # type: ignore

    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)  # type: ignore
        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

    if hasattr(args, "posonlyargs"):
        for i, arg in enumerate(args.posonlyargs):  # type: ignore
            if defaults[i] is Parameter.empty:
                default = Parameter.empty
            else:
                default = DefaultValue(ast_unparse(defaults[i], code))  # type: ignore

            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  # NOQA

        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  # NOQA
        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: Optional[str] = None
) -> Optional[str]:
    """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) -> Optional[str]:
        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: Optional[str] = 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
