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

'''
Beartype **import path hook context managers** (i.e., data structure caching
package names on behalf of the higher-level :func:`beartype.claw._clawmain`
submodule, which beartype import path hooks internally created by that submodule
subsequently lookup when deciding whether or not (and how) to decorate by
:func:`beartype.beartype` the currently imported user-specific submodule).

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

# ....................{ IMPORTS                            }....................
from beartype.claw._clawstate import (
    claw_lock,
    claw_state,
)
from beartype.claw._pkg.clawpkgtrie import (
    die_if_packages_trie,
    remove_beartype_pathhook_unless_packages_trie,
)
from beartype.typing import (
    Iterator,
    Optional,
)
from beartype._conf.confcls import (
    BEARTYPE_CONF_DEFAULT,
    BeartypeConf,
)
from contextlib import contextmanager

# ....................{ CONTEXTS                           }....................
#FIXME: Unit test us up, please.
@contextmanager
def beartyping(
    # Optional keyword-only parameters.
    *,
    conf: BeartypeConf = BEARTYPE_CONF_DEFAULT,
) -> Iterator[None]:
    '''
    Context manager temporarily registering a new **universal beartype import
    path hook** (i.e., callable inserted to the front of the standard
    :mod:`sys.path_hooks` list recursively decorating *all* typed callables and
    classes of *all* submodules of *all* packages on the first importation of
    those submodules with the :func:`beartype.beartype` decorator, wrapping
    those callables and classes with performant runtime type-checking).

    Specifically, this context manager (in order):

    #. Temporarily registers this hook by calling the public
       :func:`beartype.claw.beartype_all` function.
    #. Runs the body of the caller-defined ``with beartyping(...):`` block.
    #. Unregisters the hook registered by the prior call to that function.

    This context manager is thread-safe.

    Parameters
    ----------
    conf : BeartypeConf, optional
        **Beartype configuration** (i.e., dataclass configuring the
        :mod:`beartype.beartype` decorator for *all* decoratable objects
        recursively decorated by the path hook added by this function).
        Defaults to ``BeartypeConf()``, the default :math:`O(1)` configuration.

    Yields
    ------
    None
        This context manager yields *no* objects.

    Raises
    ------
    BeartypeClawHookException
        If the passed ``conf`` parameter is *not* a beartype configuration
        (i.e., :class:`.BeartypeConf` instance).

    See Also
    --------
    :func:`beartype.claw.beartype_all`
        Arguably unsafer alternative to this function globalizing the effect of
        this function to *all* imports performed anywhere.
    '''

    # Avoid circular import dependencies.
    from beartype.claw import beartype_all

    # Prior global beartype configuration registered by a prior call to the
    # beartype_all() function if any *OR* "None" otherwise.
    packages_trie_conf_if_hooked_old: Optional[BeartypeConf] = None

    # Attempt to...
    try:
        # With a "beartype.claw"-specific thread-safe reentrant lock...
        with claw_lock:
            # Store the prior global beartype configuration if any.
            packages_trie_conf_if_hooked_old = (
                claw_state.packages_trie.conf_if_hooked)

            # Prevent the beartype_all() function from raising an exception on
            # conflicting registrations of beartype configurations.
            claw_state.packages_trie.conf_if_hooked = None

        # Globalize the passed beartype configuration.
        beartype_all(conf=conf)

        # Defer to the caller body of the parent "with beartyping(...):" block.
        yield
    # After doing so (regardless of whether doing so raised an exception)...
    finally:
        # With a "beartype.claw"-specific thread-safe reentrant lock...
        with claw_lock:
            # If the current global beartype configuration is still the passed
            # beartype configuration, then the caller's body of the parent "with
            # beartyping(...):" block has *NOT* itself called the beartype_all()
            # function with a conflicting beartype configuration. In this
            # case...
            if claw_state.packages_trie.conf_if_hooked == conf:
                # Restore the prior global beartype configuration if any.
                claw_state.packages_trie.conf_if_hooked = (
                    packages_trie_conf_if_hooked_old)

                # Possibly remove our beartype import path hook added by the
                # above call to beartype_all() if *NO* packages are registered.
                remove_beartype_pathhook_unless_packages_trie()
            # Else, the caller's body of the parent "with beartyping(...):"
            # block has itself called the beartype_all() function with a
            # conflicting beartype configuration. In this case, preserve that
            # configuration as is.


#FIXME: Unit test us up, please.
@contextmanager
def packages_trie_cleared() -> Iterator[None]:
    '''
    Test-specific context manager reverting (i.e., clearing, resetting) the
    :data:`beartype.claw._pkg.clawpkgtrie.packages_trie` global back to its
    initial state *after* running the body of the caller-defined ``with
    beartyping(...):`` block.

    This context manager is thread-safe.

    Caveats
    -------
    **This context manager is intentionally hidden from users as a private
    attribute of this submodule** rather than publicly exported. Why? Because
    this context manager is *only* intended to be invoked by unit and
    integration tests in our test suite.

    Yields
    ------
    None
        This context manager yields *no* objects.
    '''

    # If one or more packages are still registered by a prior call to a beartype
    # import hook, raise an exception.
    die_if_packages_trie()
    # Else, *NO* packages are still registered.

    # Perform the caller-defined body of the parent "with" statement.
    try:
        yield
    # After doing so, regardless of whether doing so raised an exception...
    finally:
        # print(f'claw_state [after test]: {repr(claw_state)}')

        # With a submodule-specific thread-safe reentrant lock, reset our import
        # hook state back to its initial defaults.
        with claw_lock:
            claw_state.reinit()
