#!/usr/bin/env python3
# --------------------( LICENSE                            )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.

'''
Project-wide **callable testers** (i.e., utility functions dynamically
validating and inspecting various properties of passed callables).

This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS                            }....................
from beartype.roar._roarexc import _BeartypeUtilCallableException
from beartype.typing import Any
from beartype._cave._cavefast import MethodBoundInstanceOrClassType
from beartype._data.hint.datahintfactory import TypeGuard
from beartype._data.hint.datahinttyping import (
    Codeobjable,
    TypeException,
)
from beartype._util.cache.utilcachecall import callable_cached
from beartype._util.func.arg.utilfuncargget import (
    get_func_args_nonvariadic_len)
from beartype._util.func.arg.utilfuncargtest import (
    is_func_arg_variadic_positional,
    is_func_arg_variadic_keyword,
)
from beartype._util.func.utilfunccodeobj import get_func_codeobj_or_none
from collections.abc import Callable
from inspect import (
    CO_ASYNC_GENERATOR,
    CO_COROUTINE,
    CO_GENERATOR,
)

# ....................{ CONSTANTS                          }....................
FUNC_NAME_LAMBDA = '<lambda>'
'''
Default name of all **pure-Python lambda functions** (i.e., function declared
as a ``lambda`` expression embedded in a larger statement rather than as a
full-blown ``def`` statement).

Python initializes the names of *all* lambda functions to this lambda-specific
placeholder string on lambda definition.

Caveats
-------
**Usage of this placeholder to differentiate lambda from non-lambda callables
invites false positives in unlikely edge cases.** Technically, malicious third
parties may externally change the name of any lambda function *after* defining
that function. Pragmatically, no one sane should ever do such a horrible thing.
While predictably absurd, this is also the only efficient (and thus sane) means
of differentiating lambda from non-lambda callables. Alternatives require
AST-based parsing, which comes with its own substantial caveats, concerns,
edge cases, and false positives. If you must pick your poison, pick this one.
'''

# ....................{ RAISERS                            }....................
def die_unless_func_python(
    # Mandatory parameters.
    func: Codeobjable,

    # Optional parameters.
    exception_cls: TypeException = _BeartypeUtilCallableException,
    exception_prefix: str = '',
) -> None:
    '''
    Raise an exception if the passed callable is **C-based** (i.e., implemented
    in C as either a builtin bundled with the active Python interpreter *or*
    third-party C extension function).

    Equivalently, this validator raises an exception unless the passed function
    is **pure-Python** (i.e., implemented in Python as either a function or
    method).

    Parameters
    ----------
    func : Codeobjable
        Callable to be inspected.
    exception_cls : TypeException, optional
        Type of exception to be raised. Defaults to
        :class:`._BeartypeUtilCallableException`.
    exception_prefix : str, optional
        Human-readable label prefixing the representation of this object in the
        exception message. Defaults to the empty string.

    Raises
    ------
    exception_cls
         If the passed callable is C-based.

    See Also
    --------
    :func:`.is_func_python`
        Further details.
    '''

    # If that callable is *NOT* pure-Python, raise an exception.
    if not is_func_python(func):
        assert isinstance(exception_cls, type), (
            f'{repr(exception_cls)} not class.')
        assert issubclass(exception_cls, Exception), (
            f'{repr(exception_cls)} not exception subclass.')
        assert isinstance(exception_prefix, str), (
            f'{repr(exception_prefix)} not string.')

        # If that callable is uncallable, raise an appropriate exception.
        if not callable(func):
            raise exception_cls(f'{exception_prefix}{repr(func)} not callable.')
        # Else, that callable is callable.

        # Raise a human-readable exception.
        raise exception_cls(
            f'{exception_prefix}{repr(func)} not pure-Python function.')
    # Else, that callable is pure-Python.

# ....................{ RAISERS ~ descriptors              }....................
#FIXME: Unit test us up, please.
def die_unless_func_boundmethod(
    # Mandatory parameters.
    func: Any,

    # Optional parameters.
    exception_cls: TypeException = _BeartypeUtilCallableException,
    exception_prefix: str = '',
) -> None:
    '''
    Raise an exception unless the passed object is a **C-based bound instance
    method descriptor** callable implicitly instantiated and assigned on the
    instantiation of an object whose class declares an instance function (whose
    first parameter is typically named ``self``) as an instance variable of that
    object such that that callable unconditionally passes that object as the
    value of that first parameter on all calls to that callable).

    Parameters
    ----------
    func : Any
        Object to be inspected.
    exception_cls : TypeException, optional
        Type of exception to be raised. Defaults to
        :class:`._BeartypeUtilCallableException`.
    exception_prefix : str, optional
        Human-readable label prefixing the representation of this object in the
        exception message. Defaults to the empty string.

    Raises
    ------
    exception_cls
         If the passed object is *not* a bound method descriptor.

    See Also
    --------
    :func:`.is_func_boundmethod`
        Further details.
    '''

    # If this object is *NOT* a bound method descriptor, raise an exception.
    if not is_func_boundmethod(func):
        assert isinstance(exception_cls, type), (
            f'{repr(exception_cls)} not class.')
        assert issubclass(exception_cls, Exception), (
            f'{repr(exception_cls)} not exception subclass.')
        assert isinstance(exception_prefix, str), (
            f'{repr(exception_prefix)} not string.')

        # Raise a human-readable exception.
        raise exception_cls(
            f'{exception_prefix}{repr(func)} not '
            f'C-based bound instance method descriptor.'
        )
    # Else, this object is a bound method descriptor.


def die_unless_func_classmethod(
    # Mandatory parameters.
    func: Any,

    # Optional parameters.
    exception_cls: TypeException = _BeartypeUtilCallableException,
    exception_prefix: str = '',
) -> None:
    '''
    Raise an exception unless the passed object is a **C-based unbound class
    method descriptor** (i.e., method decorated by the builtin
    :class:`classmethod` decorator, yielding a non-callable instance of that
    :class:`classmethod` decorator class implemented in low-level C and
    accessible via the low-level :attr:`object.__dict__` dictionary rather than
    as class or instance attributes).

    Parameters
    ----------
    func : Any
        Object to be inspected.
    exception_cls : TypeException, optional
        Type of exception to be raised. Defaults to
        :class:`._BeartypeUtilCallableException`.
    exception_prefix : str, optional
        Human-readable label prefixing the representation of this object in the
        exception message. Defaults to the empty string.

    Raises
    ------
    exception_cls
         If the passed object is *not* a class method descriptor.

    See Also
    --------
    :func:`.is_func_classmethod`
        Further details.
    '''

    # If this object is *NOT* a class method descriptor, raise an exception.
    if not is_func_classmethod(func):
        assert isinstance(exception_cls, type), (
            f'{repr(exception_cls)} not class.')
        assert issubclass(exception_cls, Exception), (
            f'{repr(exception_cls)} not exception subclass.')
        assert isinstance(exception_prefix, str), (
            f'{repr(exception_prefix)} not string.')

        # Raise a human-readable exception.
        raise exception_cls(
            f'{exception_prefix}{repr(func)} not '
            f'C-based unbound class method descriptor.'
        )
    # Else, this object is a class method descriptor.


def die_unless_func_property(
    # Mandatory parameters.
    func: Any,

    # Optional parameters.
    exception_cls: TypeException = _BeartypeUtilCallableException,
    exception_prefix: str = '',
) -> None:
    '''
    Raise an exception unless the passed object is a **C-based unbound property
    method descriptor** (i.e., method decorated by the builtin :class:`property`
    decorator, yielding a non-callable instance of that :class:`property`
    decorator class implemented in low-level C and accessible as a class rather
    than instance attribute).

    Parameters
    ----------
    func : Any
        Object to be inspected.
    exception_cls : TypeException, optional
        Type of exception to be raised. Defaults to
        :property:`_BeartypeUtilCallableException`.
    exception_prefix : str, optional
        Human-readable label prefixing the representation of this object in the
        exception message. Defaults to the empty string.

    Raises
    ------
    exception_cls
         If the passed object is *not* a property method descriptor.

    See Also
    --------
    :func:`.is_func_property`
        Further details.
    '''

    # If this object is *NOT* a property method descriptor, raise an exception.
    if not is_func_property(func):
        assert isinstance(exception_cls, type), (
            f'{repr(exception_cls)} not class.')
        assert issubclass(exception_cls, Exception), (
            f'{repr(exception_cls)} not exception subclass.')
        assert isinstance(exception_prefix, str), (
            f'{repr(exception_prefix)} not string.')

        # Raise a human-readable exception.
        raise exception_cls(
            f'{exception_prefix}{repr(func)} not '
            f'C-based unbound property method descriptor.'
        )
    # Else, this object is a property method descriptor.


def die_unless_func_staticmethod(
    # Mandatory parameters.
    func: Any,

    # Optional parameters.
    exception_cls: TypeException = _BeartypeUtilCallableException,
    exception_prefix: str = '',
) -> None:
    '''
    Raise an exception unless the passed object is a **C-based unbound static
    method descriptor** (i.e., method decorated by the builtin
    :class:`staticmethod` decorator, yielding a non-callable instance of that
    :class:`staticmethod` decorator class implemented in low-level C and
    accessible via the low-level :attr:`object.__dict__` dictionary rather than
    as class or instance attributes).

    Parameters
    ----------
    func : Any
        Object to be inspected.
    exception_cls : TypeException, optional
        Type of exception to be raised. Defaults to
        :static:`_BeartypeUtilCallableException`.
    exception_prefix : str, optional
        Human-readable label prefixing the representation of this object in the
        exception message. Defaults to the empty string.

    Raises
    ------
    exception_cls
         If the passed object is *not* a static method descriptor.

    See Also
    --------
    :func:`.is_func_staticmethod`
        Further details.
    '''

    # If this object is *NOT* a static method descriptor, raise an exception.
    if not is_func_staticmethod(func):
        assert isinstance(exception_cls, type), (
            f'{repr(exception_cls)} not class.')
        assert issubclass(exception_cls, Exception), (
            f'{repr(exception_cls)} not exception subclass.')
        assert isinstance(exception_prefix, str), (
            f'{repr(exception_prefix)} not string.')

        # Raise a human-readable exception.
        raise exception_cls(
            f'{exception_prefix}{repr(func)} not '
            f'C-based unbound static method descriptor.'
        )
    # Else, this object is a static method descriptor.

# ....................{ TESTERS                            }....................
def is_func_lambda(func: Any) -> TypeGuard[Callable]:
    '''
    :data:`True` only if the passed object is a **pure-Python lambda function**
    (i.e., function declared as a ``lambda`` expression embedded in a larger
    statement rather than as a full-blown ``def`` statement).

    Parameters
    ----------
    func : object
        Object to be inspected.

    Returns
    -------
    bool
        :data:`True` only if this object is a pure-Python lambda function.
    '''

    # Return true only if this both...
    return (
        # This callable is pure-Python *AND*...
        is_func_python(func) and
        # This callable's name is the lambda-specific placeholder name
        # initially given by Python to *ALL* lambda functions. Technically,
        # this name may be externally changed by malicious third parties after
        # the declaration of this lambda. Pragmatically, no one sane would ever
        # do such a horrible thing. Would they!?!?
        #
        # While predictably absurd, this is also the only efficient (and thus
        # sane) means of differentiating lambda from non-lambda callables.
        # Alternatives require AST-based parsing, which comes with its own
        # substantial caveats, concerns, and edge cases.
        func.__name__ == FUNC_NAME_LAMBDA
    )


def is_func_python(func: object) -> TypeGuard[Callable]:
    '''
    :data:`True` only if the passed object is a **pure-Python callable** (i.e.,
    implemented in Python as either a function or method rather than in C as
    either a builtin bundled with the active Python interpreter *or*
    third-party C extension function).

    Parameters
    ----------
    func : object
        Object to be inspected.

    Returns
    -------
    bool
        :data:`True` only if this object is a pure-Python callable.
    '''

    # Return true only if a pure-Python code object underlies this object.
    # C-based callables are associated with *NO* code objects.
    return get_func_codeobj_or_none(func) is not None

# ....................{ TESTERS ~ descriptor               }....................
#FIXME: Unit test us up, please.
def is_func_boundmethod(func: Any) -> TypeGuard[MethodBoundInstanceOrClassType]:
    '''
    :data:`True` only if the passed object is a **C-based bound instance method
    descriptor** (i.e., callable implicitly instantiated and assigned on the
    instantiation of an object whose class declares an instance function (whose
    first parameter is typically named ``self``) as an instance variable of that
    object such that that callable unconditionally passes that object as the
    value of that first parameter on all calls to that callable).

    Caveats
    -------
    Instance method objects are *only* directly accessible as instance
    attributes. When accessed as either class attributes *or* via the low-level
    :attr:`object.__dict__` dictionary, instance methods are only functions
    (i.e., instances of the standard :class:`beartype.cave.FunctionType` type).

    Instance method objects are callable. Indeed, the callability of instance
    method objects is the entire point of instance method objects.

    Parameters
    ----------
    func : object
        Object to be inspected.

    Returns
    -------
    bool
        :data:`True` only if this object is a C-based bound instance method
        descriptor.
    '''

    # Only the penitent one-liner shall pass.
    return isinstance(func, MethodBoundInstanceOrClassType)


def is_func_classmethod(func: Any) -> TypeGuard[classmethod]:
    '''
    :data:`True` only if the passed object is a **C-based unbound class method
    descriptor** (i.e., method decorated by the builtin :class:`classmethod`
    decorator, yielding a non-callable instance of that :class:`classmethod`
    decorator class implemented in low-level C and accessible via the
    low-level :attr:`object.__dict__` dictionary rather than as class or
    instance attributes).

    Caveats
    -------
    Class method objects are *only* directly accessible via the low-level
    :attr:`object.__dict__` dictionary. When accessed as class or instance
    attributes, class methods reduce to instances of the standard
    :class:`MethodBoundInstanceOrClassType` type.

    Class method objects are *not* callable, as their implementations fail to
    define the ``__call__`` dunder method.

    Parameters
    ----------
    func : object
        Object to be inspected.

    Returns
    -------
    bool
        :data:`True` only if this object is a C-based unbound class method
        descriptor.
    '''

    # Now you too have seen the pure light of the one-liner.
    return isinstance(func, classmethod)


def is_func_property(func: Any) -> TypeGuard[property]:
    '''
    :data:`True` only if the passed object is a **C-based unbound property
    method descriptor** (i.e., method decorated by the builtin :class:`property`
    decorator, yielding a non-callable instance of that :class:`property`
    decorator class implemented in low-level C and accessible as a class rather
    than instance attribute).

    Caveats
    -------
    Property objects are directly accessible both as class attributes *and* via
    the low-level :attr:`object.__dict__` dictionary. Property objects are *not*
    accessible as instance attributes, for hopefully obvious reasons.

    Property objects are *not* callable, as their implementations fail to define
    the ``__call__`` dunder method.

    Parameters
    ----------
    func : object
        Object to be inspected.

    Returns
    -------
    bool
        :data:`True` only if this object is a pure-Python property.
    '''

    # We rejoice in the shared delight of one-liners.
    return isinstance(func, property)


def is_func_staticmethod(func: Any) -> TypeGuard[staticmethod]:
    '''
    :data:`True` only if the passed object is a **C-based unbound static method
    descriptor** (i.e., method decorated by the builtin :class:`staticmethod`
    decorator, yielding a non-callable instance of that :class:`staticmethod`
    decorator class implemented in low-level C and accessible via the low-level
    :attr:`object.__dict__` dictionary rather than as class or instance
    attributes).

    Caveats
    -------
    Static method objects are *only* directly accessible via the low-level
    :attr:`object.__dict__` dictionary. When accessed as class or instance
    attributes, static methods reduce to instances of the standard
    :class:`FunctionType` type.

    Static method objects are *not* callable, as their implementations fail to
    define the ``__call__`` dunder method.

    Parameters
    ----------
    func : object
        Object to be inspected.

    Returns
    -------
    bool
        :data:`True` only if this object is a pure-Python static method.
    '''

    # Does the one-liner have Buddhahood? Mu.
    return isinstance(func, staticmethod)

# ....................{ TESTERS ~ async                    }....................
def is_func_async(func: object) -> TypeGuard[Callable]:
    '''
    :data:`True` only if the passed object is an **asynchronous callable
    factory** (i.e., awaitable factory callable implicitly creating and
    returning an awaitable object (i.e., satisfying the
    :class:`collections.abc.Awaitable` protocol) by being declared via the
    ``async def`` syntax and thus callable *only* when preceded by comparable
    ``await`` syntax).

    Parameters
    ----------
    func : object
        Object to be inspected.

    Returns
    -------
    bool
        :data:`True` only if this object is an asynchronous callable.

    See Also
    --------
    :func:`inspect.iscoroutinefunction`
    :func:`inspect.isasyncgenfunction`
        Stdlib functions strongly inspiring this implementation.
    '''

    # Code object underlying this pure-Python callable if any *OR* "None".
    #
    # Note this tester intentionally:
    # * Inlines the tests performed by the is_func_coro() and
    #   is_func_async_generator() testers for efficiency.
    # * Calls the get_func_codeobj_or_none() with "is_unwrap" disabled
    #   rather than enabled. Why? Because the asynchronicity of this possibly
    #   higher-level wrapper has *NO* relation to that of the possibly
    #   lower-level wrappee wrapped by this wrapper. Notably, it is both
    #   feasible and commonplace for third-party decorators to enable:
    #   * Synchronous callables to be called asynchronously by wrapping
    #     synchronous callables with asynchronous closures.
    #   * Asynchronous callables to be called synchronously by wrapping
    #     asynchronous callables with synchronous closures. Indeed, our
    #     top-level "conftest.py" pytest plugin does exactly this -- enabling
    #     asynchronous tests to be safely called by pytest's currently
    #     synchronous framework.
    func_codeobj = get_func_codeobj_or_none(func)

    # If this object is *NOT* a pure-Python callable, immediately return false.
    if func_codeobj is None:
        return False
    # Else, this object is a pure-Python callable.

    # Bit field of OR-ed binary flags describing this callable.
    func_codeobj_flags = func_codeobj.co_flags

    # Return true only if these flags imply this callable to be either...
    return (
        # An asynchronous coroutine *OR*...
        func_codeobj_flags & CO_COROUTINE != 0 or
        # An asynchronous generator.
        func_codeobj_flags & CO_ASYNC_GENERATOR != 0
    )


def is_func_coro(func: object) -> TypeGuard[Callable]:
    '''
    :data:`True` only if the passed object is an **asynchronous coroutine
    factory** (i.e., awaitable callable containing *no* ``yield`` expression
    implicitly creating and returning an awaitable object (i.e., satisfying the
    :class:`collections.abc.Awaitable` protocol) by being declared via the
    ``async def`` syntax and thus callable *only* when preceded by comparable
    ``await`` syntax).

    Parameters
    ----------
    func : object
        Object to be inspected.

    Returns
    -------
    bool
        :data:`True` only if this object is an asynchronous coroutine factory.

    See Also
    --------
    :func:`inspect.iscoroutinefunction`
        Stdlib function strongly inspiring this implementation.
    '''

    # Code object underlying this pure-Python callable if any *OR* "None".
    func_codeobj = get_func_codeobj_or_none(func)

    # Return true only if...
    return (
        # This object is a pure-Python callable *AND*...
        func_codeobj is not None and
        # This callable's code object implies this callable to be an
        # asynchronous coroutine.
        func_codeobj.co_flags & CO_COROUTINE != 0
    )


def is_func_async_generator(func: object) -> TypeGuard[Callable]:
    '''
    :data:`True` only if the passed object is an **asynchronous generator
    factory** (i.e., awaitable callable containing one or more ``yield``
    expressions implicitly creating and returning an awaitable object (i.e.,
    satisfying the :class:`collections.abc.Awaitable` protocol) by being
    declared via the ``async def`` syntax and thus callable *only* when preceded
    by comparable ``await`` syntax).

    Parameters
    ----------
    func : object
        Object to be inspected.

    Returns
    -------
    bool
        :data:`True` only if this object is an asynchronous generator.

    See Also
    --------
    :func:`inspect.isasyncgenfunction`
        Stdlib function strongly inspiring this implementation.
    '''

    # Code object underlying this pure-Python callable if any *OR* "None".
    func_codeobj = get_func_codeobj_or_none(func)

    # Return true only if...
    return (
        # This object is a pure-Python callable *AND*...
        func_codeobj is not None and
        # This callable's code object implies this callable to be an
        # asynchronous generator.
        func_codeobj.co_flags & CO_ASYNC_GENERATOR != 0
    )

# ....................{ TESTERS ~ sync                     }....................
def is_func_sync_generator(func: object) -> TypeGuard[Callable]:
    '''
    :data:`True` only if the passed object is an **synchronous generator
    factory** (i.e., awaitable callable containing one or more ``yield``
    expressions implicitly creating and returning a generator object (i.e.,
    satisfying the :class:`collections.abc.Generator` protocol) by being
    declared via the ``def`` rather than ``async def`` syntax).

    Parameters
    ----------
    func : object
        Object to be inspected.

    Returns
    -------
    bool
        :data:`True` only if this object is a synchronous generator.

    See Also
    --------
    :func:`inspect.isgeneratorfunction`
        Stdlib function strongly inspiring this implementation.
    '''

    # If this object is uncallable, immediately return False.
    #
    # Note this test is explicitly required to differentiate synchronous
    # generator callables from synchronous generator objects (i.e., the objects
    # they implicitly create and return). Whereas both asynchronous coroutine
    # objects *AND* asynchronous generator objects do *NOT* contain code
    # objects whose "CO_COROUTINE" and "CO_ASYNC_GENERATOR" flags are non-zero,
    # synchronous generator objects do contain code objects whose
    # "CO_GENERATOR" flag is non-zero. This implies synchronous generator
    # callables to create and return synchronous generator objects that are
    # themselves technically valid synchronous generator callables, which is
    # absurd. We prohibit this ambiguity by differentiating the two here.
    if not callable(func):
        return False
    # Else, this object is callable.

    # Code object underlying this pure-Python callable if any *OR* "None".
    func_codeobj = get_func_codeobj_or_none(func)

    # Return true only if...
    return (
        # This object is a pure-Python callable *AND*...
        func_codeobj is not None and
        # This callable's code object implies this callable to be a
        # synchronous generator.
        func_codeobj.co_flags & CO_GENERATOR != 0
    )

# ....................{ TESTERS : nested                   }....................
def is_func_nested(func: Callable) -> bool:
    '''
    :data:`True` only if the passed callable is **nested** (i.e., a pure-Python
    callable declared in the body of another pure-Python callable or class).

    Equivalently, this tester returns :data:`True` only if that callable is
    either:

    * A closure, which by definition is nested inside another callable.
    * A method, which by definition is nested inside its class.
    * A **nested non-closure function** (i.e., a closure-like function that does
      *not* reference local attributes of the parent callable enclosing that
      function and is thus technically *not* a closure): e.g.,

      .. code-block:: python

         def muh_parent_callable():           # <-- parent callable
             def muh_nested_callable(): pass  # <-- nested non-closure function
             return muh_nested_callable

    Parameters
    ----------
    func : Callable
        Callable to be inspected.

    Returns
    -------
    bool
        :data:`True` only if this callable is nested.
    '''

    # Return true only if either...
    return (
        # That callable is a closure (in which case that closure is necessarily
        # nested inside another callable) *OR*...
        #
        # Note that this tester intentionally tests for whether that callable is
        # a closure first, as doing so efficiently reduces to a constant-time
        # attribute test -- whereas the following test for non-closure nested
        # callables inefficiently requires a linear-time string search.
        is_func_closure(func) or
        # The fully-qualified name of that callable contains one or more "."
        # delimiters, each signifying a nested lexical scope. Since *ALL*
        # callables (i.e., both pure-Python and C-based) define a non-empty
        # "__qualname__" dunder variable containing at least their unqualified
        # names, this simplistic test is guaranteed to be safe.
        #
        # Note this tester intentionally tests for the general-purpose existence
        # of a "." delimiter rather than the special-cased existence of a
        # ".<locals>." placeholder substring. Why? Because there are two types
        # of nested callables:
        # * Non-methods, which are lexically nested in a parent callable whose
        #   scope encapsulates all previously declared local variables. For
        #   unknown reasons, the unqualified names of nested non-method
        #   callables are *ALWAYS* prefixed by ".<locals>." in their
        #   "__qualname__" variables:
        #       >>> from collections.abc import Callable
        #       >>> def muh_parent_callable() -> Callable:
        #       ...     def muh_nested_callable() -> None: pass
        #       ...     return muh_nested_callable
        #       >>> muh_nested_callable = muh_parent_callable()
        #       >>> muh_parent_callable.__qualname__
        #       'muh_parent_callable'
        #       >>> muh_nested_callable.__qualname__
        #       'muh_parent_callable.<locals>.muh_nested_callable'
        # * Methods, which are lexically nested in the scope encapsulating all
        #   previously declared class variables (i.e., variables declared in
        #   class scope and thus accessible as method annotations). For unknown
        #   reasons, the unqualified names of methods are *NEVER* prefixed by
        #   ".<locals>." in their "__qualname__" variables: e.g.,
        #       >>> from typing import ClassVar
        #       >>> class MuhClass(object):
        #       ...    # Class variable declared in class scope.
        #       ...    muh_class_var: ClassVar[type] = int
        #       ...    # Instance method annotated by this class variable.
        #       ...    def muh_method(self) -> muh_class_var: return 42
        #       >>> MuhClass.muh_method.__qualname__
        #       'MuhClass.muh_method'
        '.' in func.__qualname__
    )

# ....................{ TESTERS ~ nested : closure         }....................
def is_func_closure(func: Any) -> TypeGuard[Callable]:
    '''
    :data:`True` only if the passed callable is a **closure** (i.e., nested
    callable accessing one or more variables declared by the parent callable
    also declaring that callable).

    Note that all closures are necessarily nested callables but that the
    converse is *not* necessarily the case. In particular, a nested callable
    accessing *no* variables declared by the parent callable also declaring that
    callable is *not* a closure; it's simply a nested callable.

    Parameters
    ----------
    func : Callable
        Callable to be inspected.

    Returns
    -------
    bool
        :data:`True` only if this callable is a closure.
    '''

    # Return true only if that callable defines the closure-specific
    # "__closure__" dunder variable whose value is either:
    # * If that callable is a closure, a tuple of zero or more cell variables.
    # * If that callable is a pure-Python non-closure, "None".
    # * If that callable is C-based, undefined.
    return getattr(func, '__closure__', None) is not None

# ....................{ TESTERS ~ wrapper                  }....................
def is_func_wrapper(func: Any) -> TypeGuard[Callable]:
    '''
    :data:`True` only if the passed object is a **callable wrapper** (i.e.,
    callable decorated by the standard :func:`functools.wraps` decorator for
    wrapping a pure-Python callable with additional functionality defined by a
    higher-level decorator).

    Note that this tester returns :data:`True` for both pure-Python and C-based
    callable wrappers. As an example of the latter, the standard
    :func:`functools.lru_cache` decorator creates and returns low-level C-based
    callable wrappers of the private type :class:`functools._lru_cache_wrapper`
    wrapping pure-Python callables.

    Parameters
    ----------
    func : object
        Object to be inspected.

    Returns
    -------
    bool
        :data:`True` only if this object is a callable wrapper.
    '''

    # Return true only if this object defines a dunder attribute uniquely
    # specific to the @functools.wraps decorator.
    #
    # Technically, *ANY* callable (including non-wrappers *NOT* created by the
    # @functools.wraps decorator) could trivially define this attribute; ergo,
    # this invites the possibility of false positives. Pragmatically, doing so
    # would violate ad-hoc standards and real-world practice across the
    # open-source ecosystem; ergo, this effectively excludes false positives.
    return hasattr(func, '__wrapped__')


@callable_cached
def is_func_wrapper_isomorphic(func: Any) -> TypeGuard[Callable]:
    '''
    :data:`True` only if the passed object is an **isomorphic wrapper** (i.e.,
    callable decorated by the standard :func:`functools.wraps` decorator for
    wrapping a pure-Python callable with additional functionality defined by a
    higher-level decorator such that that wrapper isomorphically preserves both
    the number and types of all passed parameters and returns by accepting only
    a variadic positional argument and a variadic keyword argument).

    This tester enables callers to detect when a user-defined callable has been
    decorated by an isomorphic decorator, which constitutes *most* real-world
    decorators of interest.

    This tester is memoized for efficiency.

    Caveats
    -------
    **This tester is merely a heuristic** -- albeit a reasonably robust
    heuristic likely to succeed in almost all real-world use cases. Nonetheless,
    this tester *could* return false positives and negatives in edge cases.

    Parameters
    ----------
    func : object
        Object to be inspected.

    Returns
    -------
    bool
        :data:`True` only if this object is an isomorphic decorator wrapper.
    '''

    # If the passed callable is *NOT* a wrapper, immediately return false.
    if not is_func_wrapper(func):
        return False
    # Else, that callable is a wrapper.

    # Code object underlying that callable as is (rather than possibly unwrapped
    # to another code object entirely) if that callable is pure-Python *OR*
    # "None" otherwise (i.e., if that callable is C-based).
    func_codeobj = get_func_codeobj_or_none(func)

    # Return true only if...
    return (
        # That callable is pure-Python *AND*...
        func_codeobj is not None and
        # That callable accepts a variadic positional argument *AND*...
        is_func_arg_variadic_positional(func_codeobj) and
        # That callable accepts a variadic keyword argument *AND*...
        is_func_arg_variadic_keyword(func_codeobj) and
        # That callable accepts *NO* non-variadic arguments.
        get_func_args_nonvariadic_len(func_codeobj) == 0
    )
