"""
    test_util_inspect
    ~~~~~~~~~~~~~~~

    Tests util.inspect functions.

    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
    :license: BSD, see LICENSE for details.
"""

import ast
import datetime
import enum
import functools
import sys
import types
from inspect import Parameter

import pytest

from sphinx.util import inspect
from sphinx.util.inspect import TypeAliasNamespace, stringify_signature


def test_TypeAliasNamespace():
    import logging.config
    type_alias = TypeAliasNamespace({'logging.Filter': 'MyFilter',
                                     'logging.Handler': 'MyHandler',
                                     'logging.handlers.SyslogHandler': 'MySyslogHandler'})

    assert type_alias['logging'].Filter == 'MyFilter'
    assert type_alias['logging'].Handler == 'MyHandler'
    assert type_alias['logging'].handlers.SyslogHandler == 'MySyslogHandler'
    assert type_alias['logging'].Logger == logging.Logger
    assert type_alias['logging'].config == logging.config

    with pytest.raises(KeyError):
        assert type_alias['log']

    with pytest.raises(KeyError):
        assert type_alias['unknown']


def test_signature():
    # literals
    with pytest.raises(TypeError):
        inspect.signature(1)

    with pytest.raises(TypeError):
        inspect.signature('')

    # builtins are supported on a case-by-case basis, depending on whether
    # they define __text_signature__
    if getattr(list, '__text_signature__', None):
        sig = inspect.stringify_signature(inspect.signature(list))
        assert sig == '(iterable=(), /)'
    else:
        with pytest.raises(ValueError):
            inspect.signature(list)

    # normal function
    def func(a, b, c=1, d=2, *e, **f):
        pass

    sig = inspect.stringify_signature(inspect.signature(func))
    assert sig == '(a, b, c=1, d=2, *e, **f)'


def test_signature_partial():
    def fun(a, b, c=1, d=2):
        pass
    p = functools.partial(fun, 10, c=11)

    sig = inspect.signature(p)
    assert stringify_signature(sig) == '(b, *, c=11, d=2)'


def test_signature_methods():
    class Foo:
        def meth1(self, arg1, **kwargs):
            pass

        @classmethod
        def meth2(cls, arg1, *args, **kwargs):
            pass

        @staticmethod
        def meth3(arg1, *args, **kwargs):
            pass

    @functools.wraps(Foo().meth1)
    def wrapped_bound_method(*args, **kwargs):
        pass

    # unbound method
    sig = inspect.signature(Foo.meth1)
    assert stringify_signature(sig) == '(self, arg1, **kwargs)'

    sig = inspect.signature(Foo.meth1, bound_method=True)
    assert stringify_signature(sig) == '(arg1, **kwargs)'

    # bound method
    sig = inspect.signature(Foo().meth1)
    assert stringify_signature(sig) == '(arg1, **kwargs)'

    # class method
    sig = inspect.signature(Foo.meth2)
    assert stringify_signature(sig) == '(arg1, *args, **kwargs)'

    sig = inspect.signature(Foo().meth2)
    assert stringify_signature(sig) == '(arg1, *args, **kwargs)'

    # static method
    sig = inspect.signature(Foo.meth3)
    assert stringify_signature(sig) == '(arg1, *args, **kwargs)'

    sig = inspect.signature(Foo().meth3)
    assert stringify_signature(sig) == '(arg1, *args, **kwargs)'

    # wrapped bound method
    sig = inspect.signature(wrapped_bound_method)
    assert stringify_signature(sig) == '(arg1, **kwargs)'


def test_signature_partialmethod():
    from functools import partialmethod

    class Foo:
        def meth1(self, arg1, arg2, arg3=None, arg4=None):
            pass

        def meth2(self, arg1, arg2):
            pass

        foo = partialmethod(meth1, 1, 2)
        bar = partialmethod(meth1, 1, arg3=3)
        baz = partialmethod(meth2, 1, 2)

    subject = Foo()
    sig = inspect.signature(subject.foo)
    assert stringify_signature(sig) == '(arg3=None, arg4=None)'

    sig = inspect.signature(subject.bar)
    assert stringify_signature(sig) == '(arg2, *, arg3=3, arg4=None)'

    sig = inspect.signature(subject.baz)
    assert stringify_signature(sig) == '()'


def test_signature_annotations():
    from .typing_test_data import (Node, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12,
                                   f13, f14, f15, f16, f17, f18, f19, f20, f21)

    # Class annotations
    sig = inspect.signature(f0)
    assert stringify_signature(sig) == '(x: int, y: numbers.Integral) -> None'

    # Generic types with concrete parameters
    sig = inspect.signature(f1)
    assert stringify_signature(sig) == '(x: List[int]) -> List[int]'

    # TypeVars and generic types with TypeVars
    sig = inspect.signature(f2)
    if sys.version_info < (3, 7):
        assert stringify_signature(sig) == '(x: List[T], y: List[T_co], z: T) -> List[T_contra]'
    else:
        assert stringify_signature(sig) == ('(x: List[tests.typing_test_data.T],'
                                            ' y: List[tests.typing_test_data.T_co],'
                                            ' z: tests.typing_test_data.T'
                                            ') -> List[tests.typing_test_data.T_contra]')

    # Union types
    sig = inspect.signature(f3)
    assert stringify_signature(sig) == '(x: Union[str, numbers.Integral]) -> None'

    # Quoted annotations
    sig = inspect.signature(f4)
    assert stringify_signature(sig) == '(x: str, y: str) -> None'

    # Keyword-only arguments
    sig = inspect.signature(f5)
    assert stringify_signature(sig) == '(x: int, *, y: str, z: str) -> None'

    # Keyword-only arguments with varargs
    sig = inspect.signature(f6)
    assert stringify_signature(sig) == '(x: int, *args, y: str, z: str) -> None'

    # Space around '=' for defaults
    sig = inspect.signature(f7)
    assert stringify_signature(sig) == '(x: Optional[int] = None, y: dict = {}) -> None'

    # Callable types
    sig = inspect.signature(f8)
    assert stringify_signature(sig) == '(x: Callable[[int, str], int]) -> None'

    sig = inspect.signature(f9)
    assert stringify_signature(sig) == '(x: Callable) -> None'

    # Tuple types
    sig = inspect.signature(f10)
    assert stringify_signature(sig) == '(x: Tuple[int, str], y: Tuple[int, ...]) -> None'

    # Instance annotations
    sig = inspect.signature(f11)
    assert stringify_signature(sig) == '(x: CustomAnnotation, y: 123) -> None'

    # tuple with more than two items
    sig = inspect.signature(f12)
    assert stringify_signature(sig) == '() -> Tuple[int, str, int]'

    # optional
    sig = inspect.signature(f13)
    assert stringify_signature(sig) == '() -> Optional[str]'

    # optional union
    sig = inspect.signature(f20)
    assert stringify_signature(sig) in ('() -> Optional[Union[int, str]]',
                                        '() -> Optional[Union[str, int]]')

    # Any
    sig = inspect.signature(f14)
    assert stringify_signature(sig) == '() -> Any'

    # ForwardRef
    sig = inspect.signature(f15)
    assert stringify_signature(sig) == '(x: Unknown, y: int) -> Any'

    # keyword only arguments (1)
    sig = inspect.signature(f16)
    assert stringify_signature(sig) == '(arg1, arg2, *, arg3=None, arg4=None)'

    # keyword only arguments (2)
    sig = inspect.signature(f17)
    assert stringify_signature(sig) == '(*, arg3, arg4)'

    sig = inspect.signature(f18)
    assert stringify_signature(sig) == '(self, arg1: Union[int, Tuple] = 10) -> List[Dict]'

    # annotations for variadic and keyword parameters
    sig = inspect.signature(f19)
    assert stringify_signature(sig) == '(*args: int, **kwargs: str)'

    # default value is inspect.Signature.empty
    sig = inspect.signature(f21)
    assert stringify_signature(sig) == "(arg1='whatever', arg2)"

    # type hints by string
    sig = inspect.signature(Node.children)
    assert stringify_signature(sig) == '(self) -> List[tests.typing_test_data.Node]'

    sig = inspect.signature(Node.__init__)
    assert stringify_signature(sig) == '(self, parent: Optional[tests.typing_test_data.Node]) -> None'

    # show_annotation is False
    sig = inspect.signature(f7)
    assert stringify_signature(sig, show_annotation=False) == '(x=None, y={})'

    # show_return_annotation is False
    sig = inspect.signature(f7)
    assert stringify_signature(sig, show_return_annotation=False) == '(x: Optional[int] = None, y: dict = {})'


@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.')
@pytest.mark.sphinx(testroot='ext-autodoc')
def test_signature_annotations_py38(app):
    from target.pep570 import bar, baz, foo, qux

    # case: separator at head
    sig = inspect.signature(foo)
    assert stringify_signature(sig) == '(*, a, b)'

    # case: separator in the middle
    sig = inspect.signature(bar)
    assert stringify_signature(sig) == '(a, b, /, c, d)'

    sig = inspect.signature(baz)
    assert stringify_signature(sig) == '(a, /, *, b)'

    # case: separator at tail
    sig = inspect.signature(qux)
    assert stringify_signature(sig) == '(a, b, /)'


def test_signature_from_str_basic():
    signature = '(a, b, *args, c=0, d="blah", **kwargs)'
    sig = inspect.signature_from_str(signature)
    assert list(sig.parameters.keys()) == ['a', 'b', 'args', 'c', 'd', 'kwargs']
    assert sig.parameters['a'].name == 'a'
    assert sig.parameters['a'].kind == Parameter.POSITIONAL_OR_KEYWORD
    assert sig.parameters['a'].default == Parameter.empty
    assert sig.parameters['a'].annotation == Parameter.empty
    assert sig.parameters['b'].name == 'b'
    assert sig.parameters['b'].kind == Parameter.POSITIONAL_OR_KEYWORD
    assert sig.parameters['b'].default == Parameter.empty
    assert sig.parameters['b'].annotation == Parameter.empty
    assert sig.parameters['args'].name == 'args'
    assert sig.parameters['args'].kind == Parameter.VAR_POSITIONAL
    assert sig.parameters['args'].default == Parameter.empty
    assert sig.parameters['args'].annotation == Parameter.empty
    assert sig.parameters['c'].name == 'c'
    assert sig.parameters['c'].kind == Parameter.KEYWORD_ONLY
    assert sig.parameters['c'].default == '0'
    assert sig.parameters['c'].annotation == Parameter.empty
    assert sig.parameters['d'].name == 'd'
    assert sig.parameters['d'].kind == Parameter.KEYWORD_ONLY
    assert sig.parameters['d'].default == "'blah'"
    assert sig.parameters['d'].annotation == Parameter.empty
    assert sig.parameters['kwargs'].name == 'kwargs'
    assert sig.parameters['kwargs'].kind == Parameter.VAR_KEYWORD
    assert sig.parameters['kwargs'].default == Parameter.empty
    assert sig.parameters['kwargs'].annotation == Parameter.empty
    assert sig.return_annotation == Parameter.empty


def test_signature_from_str_default_values():
    signature = ('(a=0, b=0.0, c="str", d=b"bytes", e=..., f=True, '
                 'g=[1, 2, 3], h={"a": 1}, i={1, 2, 3}, '
                 'j=lambda x, y: None, k=None, l=object(), m=foo.bar.CONSTANT)')
    sig = inspect.signature_from_str(signature)
    assert sig.parameters['a'].default == '0'
    assert sig.parameters['b'].default == '0.0'
    assert sig.parameters['c'].default == "'str'"
    assert sig.parameters['d'].default == "b'bytes'"
    assert sig.parameters['e'].default == '...'
    assert sig.parameters['f'].default == 'True'
    assert sig.parameters['g'].default == '[1, 2, 3]'
    assert sig.parameters['h'].default == "{'a': 1}"
    assert sig.parameters['i'].default == '{1, 2, 3}'
    assert sig.parameters['j'].default == 'lambda x, y: ...'
    assert sig.parameters['k'].default == 'None'
    assert sig.parameters['l'].default == 'object()'
    assert sig.parameters['m'].default == 'foo.bar.CONSTANT'


def test_signature_from_str_annotations():
    signature = '(a: int, *args: bytes, b: str = "blah", **kwargs: float) -> None'
    sig = inspect.signature_from_str(signature)
    assert list(sig.parameters.keys()) == ['a', 'args', 'b', 'kwargs']
    assert sig.parameters['a'].annotation == "int"
    assert sig.parameters['args'].annotation == "bytes"
    assert sig.parameters['b'].annotation == "str"
    assert sig.parameters['kwargs'].annotation == "float"
    assert sig.return_annotation == 'None'


def test_signature_from_str_complex_annotations():
    sig = inspect.signature_from_str('() -> Tuple[str, int, ...]')
    assert sig.return_annotation == 'Tuple[str, int, ...]'

    sig = inspect.signature_from_str('() -> Callable[[int, int], int]')
    assert sig.return_annotation == 'Callable[[int, int], int]'


def test_signature_from_str_kwonly_args():
    sig = inspect.signature_from_str('(a, *, b)')
    assert list(sig.parameters.keys()) == ['a', 'b']
    assert sig.parameters['a'].kind == Parameter.POSITIONAL_OR_KEYWORD
    assert sig.parameters['a'].default == Parameter.empty
    assert sig.parameters['b'].kind == Parameter.KEYWORD_ONLY
    assert sig.parameters['b'].default == Parameter.empty


@pytest.mark.skipif(sys.version_info < (3, 8),
                    reason='python-3.8 or above is required')
def test_signature_from_str_positionaly_only_args():
    sig = inspect.signature_from_str('(a, b=0, /, c=1)')
    assert list(sig.parameters.keys()) == ['a', 'b', 'c']
    assert sig.parameters['a'].kind == Parameter.POSITIONAL_ONLY
    assert sig.parameters['a'].default == Parameter.empty
    assert sig.parameters['b'].kind == Parameter.POSITIONAL_ONLY
    assert sig.parameters['b'].default == '0'
    assert sig.parameters['c'].kind == Parameter.POSITIONAL_OR_KEYWORD
    assert sig.parameters['c'].default == '1'


def test_signature_from_str_invalid():
    with pytest.raises(SyntaxError):
        inspect.signature_from_str('')


def test_signature_from_ast():
    signature = 'def func(a, b, *args, c=0, d="blah", **kwargs): pass'
    tree = ast.parse(signature)
    sig = inspect.signature_from_ast(tree.body[0])
    assert list(sig.parameters.keys()) == ['a', 'b', 'args', 'c', 'd', 'kwargs']
    assert sig.parameters['a'].name == 'a'
    assert sig.parameters['a'].kind == Parameter.POSITIONAL_OR_KEYWORD
    assert sig.parameters['a'].default == Parameter.empty
    assert sig.parameters['a'].annotation == Parameter.empty
    assert sig.parameters['b'].name == 'b'
    assert sig.parameters['b'].kind == Parameter.POSITIONAL_OR_KEYWORD
    assert sig.parameters['b'].default == Parameter.empty
    assert sig.parameters['b'].annotation == Parameter.empty
    assert sig.parameters['args'].name == 'args'
    assert sig.parameters['args'].kind == Parameter.VAR_POSITIONAL
    assert sig.parameters['args'].default == Parameter.empty
    assert sig.parameters['args'].annotation == Parameter.empty
    assert sig.parameters['c'].name == 'c'
    assert sig.parameters['c'].kind == Parameter.KEYWORD_ONLY
    assert sig.parameters['c'].default == '0'
    assert sig.parameters['c'].annotation == Parameter.empty
    assert sig.parameters['d'].name == 'd'
    assert sig.parameters['d'].kind == Parameter.KEYWORD_ONLY
    assert sig.parameters['d'].default == "'blah'"
    assert sig.parameters['d'].annotation == Parameter.empty
    assert sig.parameters['kwargs'].name == 'kwargs'
    assert sig.parameters['kwargs'].kind == Parameter.VAR_KEYWORD
    assert sig.parameters['kwargs'].default == Parameter.empty
    assert sig.parameters['kwargs'].annotation == Parameter.empty
    assert sig.return_annotation == Parameter.empty


def test_safe_getattr_with_default():
    class Foo:
        def __getattr__(self, item):
            raise Exception

    obj = Foo()

    result = inspect.safe_getattr(obj, 'bar', 'baz')

    assert result == 'baz'


def test_safe_getattr_with_exception():
    class Foo:
        def __getattr__(self, item):
            raise Exception

    obj = Foo()

    try:
        inspect.safe_getattr(obj, 'bar')
    except AttributeError as exc:
        assert exc.args[0] == 'bar'
    else:
        pytest.fail('AttributeError not raised')


def test_safe_getattr_with_property_exception():
    class Foo:
        @property
        def bar(self):
            raise Exception

    obj = Foo()

    try:
        inspect.safe_getattr(obj, 'bar')
    except AttributeError as exc:
        assert exc.args[0] == 'bar'
    else:
        pytest.fail('AttributeError not raised')


def test_safe_getattr_with___dict___override():
    class Foo:
        @property
        def __dict__(self):
            raise Exception

    obj = Foo()

    try:
        inspect.safe_getattr(obj, 'bar')
    except AttributeError as exc:
        assert exc.args[0] == 'bar'
    else:
        pytest.fail('AttributeError not raised')


def test_dictionary_sorting():
    dictionary = {"c": 3, "a": 1, "d": 2, "b": 4}
    description = inspect.object_description(dictionary)
    assert description == "{'a': 1, 'b': 4, 'c': 3, 'd': 2}"


def test_set_sorting():
    set_ = set("gfedcba")
    description = inspect.object_description(set_)
    assert description == "{'a', 'b', 'c', 'd', 'e', 'f', 'g'}"


def test_set_sorting_fallback():
    set_ = {None, 1}
    description = inspect.object_description(set_)
    assert description in ("{1, None}", "{None, 1}")


def test_frozenset_sorting():
    frozenset_ = frozenset("gfedcba")
    description = inspect.object_description(frozenset_)
    assert description == "frozenset({'a', 'b', 'c', 'd', 'e', 'f', 'g'})"


def test_frozenset_sorting_fallback():
    frozenset_ = frozenset((None, 1))
    description = inspect.object_description(frozenset_)
    assert description in ("frozenset({1, None})", "frozenset({None, 1})")


def test_dict_customtype():
    class CustomType:
        def __init__(self, value):
            self._value = value

        def __repr__(self):
            return "<CustomType(%r)>" % self._value

    dictionary = {CustomType(2): 2, CustomType(1): 1}
    description = inspect.object_description(dictionary)
    # Type is unsortable, just check that it does not crash
    assert "<CustomType(2)>: 2" in description


def test_object_description_enum():
    class MyEnum(enum.Enum):
        FOO = 1
        BAR = 2

    assert inspect.object_description(MyEnum.FOO) == "MyEnum.FOO"


def test_getslots():
    class Foo:
        pass

    class Bar:
        __slots__ = ['attr']

    class Baz:
        __slots__ = {'attr': 'docstring'}

    class Qux:
        __slots__ = 'attr'

    assert inspect.getslots(Foo) is None
    assert inspect.getslots(Bar) == {'attr': None}
    assert inspect.getslots(Baz) == {'attr': 'docstring'}
    assert inspect.getslots(Qux) == {'attr': None}

    with pytest.raises(TypeError):
        inspect.getslots(Bar())


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

    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

    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

    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

    # 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

    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
    assert inspect.isfunction(Base.partialmeth) is True         # partial-ed method of class
    assert inspect.isfunction(Base().meth) is False             # method of instance
    assert inspect.isfunction(builtin_func) is False            # builtin function
    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

    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
    assert inspect.isbuiltin(partial_func) is False         # partial-ed function
    assert inspect.isbuiltin(Base.meth) is False            # method of class
    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

    assert inspect.isdescriptor(Base.prop) is True      # property of class
    assert inspect.isdescriptor(Base().prop) is False   # property of instance
    assert inspect.isdescriptor(Base.meth) is True      # method of class
    assert inspect.isdescriptor(Base().meth) is True    # method of instance
    assert inspect.isdescriptor(func) is True           # function


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

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

    assert inspect.isattributedescriptor(Base.prop) is True                    # property
    assert inspect.isattributedescriptor(Base.meth) is False                   # method
    assert inspect.isattributedescriptor(Base.staticmeth) is False             # staticmethod
    assert inspect.isattributedescriptor(Base.classmeth) is False              # classmetho
    assert inspect.isattributedescriptor(Descriptor) is False                  # custom descriptor class    # NOQA
    assert inspect.isattributedescriptor(str.join) is False                    # MethodDescriptorType       # NOQA
    assert inspect.isattributedescriptor(object.__init__) is False             # WrapperDescriptorType      # NOQA
    assert inspect.isattributedescriptor(dict.__dict__['fromkeys']) is False   # ClassMethodDescriptorType  # NOQA
    assert inspect.isattributedescriptor(types.FrameType.f_locals) is True     # GetSetDescriptorType       # NOQA
    assert inspect.isattributedescriptor(datetime.timedelta.days) is True      # MemberDescriptorType       # NOQA

    try:
        # _testcapi module cannot be importable in some distro
        # refs: https://github.com/sphinx-doc/sphinx/issues/9868
        import _testcapi

        testinstancemethod = _testcapi.instancemethod(str.__repr__)
        assert inspect.isattributedescriptor(testinstancemethod) is False      # instancemethod (C-API)     # NOQA
    except ImportError:
        pass


def test_isproperty(app):
    from target.functions import func
    from target.methods import Base

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


@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
@pytest.mark.sphinx(testroot='ext-autodoc')
def test_isgenericalias(app):
    from target.genericalias import C, T
    from target.methods import Base

    assert inspect.isgenericalias(C) is True
    assert inspect.isgenericalias(T) is True
    assert inspect.isgenericalias(object()) is False
    assert inspect.isgenericalias(Base) is False


def test_unpartial():
    def func1(a, b, c):
        pass

    func2 = functools.partial(func1, 1)
    func2.__doc__ = "func2"
    func3 = functools.partial(func2, 2)  # nested partial object

    assert inspect.unpartial(func2) is func1
    assert inspect.unpartial(func3) is func1


def test_getdoc_inherited_classmethod():
    class Foo:
        @classmethod
        def meth(self):
            """
            docstring
                indented text
            """

    class Bar(Foo):
        @classmethod
        def meth(self):
            # inherited classmethod
            pass

    assert inspect.getdoc(Bar.meth, getattr, False, Bar, "meth") is None
    assert inspect.getdoc(Bar.meth, getattr, True, Bar, "meth") == Foo.meth.__doc__


def test_getdoc_inherited_decorated_method():
    class Foo:
        def meth(self):
            """
            docstring
                indented text
            """

    class Bar(Foo):
        @functools.lru_cache()
        def meth(self):
            # inherited and decorated method
            pass

    assert inspect.getdoc(Bar.meth, getattr, False, Bar, "meth") is None
    assert inspect.getdoc(Bar.meth, getattr, True, Bar, "meth") == Foo.meth.__doc__


def test_is_builtin_class_method():
    class MyInt(int):
        def my_method(self):
            pass

    assert inspect.is_builtin_class_method(MyInt, 'to_bytes')
    assert inspect.is_builtin_class_method(MyInt, '__init__')
    assert not inspect.is_builtin_class_method(MyInt, 'my_method')
    assert not inspect.is_builtin_class_method(MyInt, 'does_not_exist')
    assert not inspect.is_builtin_class_method(4, 'still does not crash')

    class ObjectWithMroAttr:
        def __init__(self, mro_attr):
            self.__mro__ = mro_attr

    assert not inspect.is_builtin_class_method(ObjectWithMroAttr([1, 2, 3]), 'still does not crash')
