Repository URL to install this package:
|
Version:
0.18.5 ▾
|
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.
'''
Beartype **import hook managers** (i.e., lower-level private-facing functions
internally driving the higher-level public facing import hooks exported by the
:mod:`beartype.claw._clawmain` submodule).
This private submodule is *not* intended for importation by downstream callers.
'''
# ....................{ IMPORTS }....................
from beartype.claw._pkg.clawpkgenum import BeartypeClawCoverage
from beartype.claw._pkg.clawpkgtrie import (
PackagesTrie,
iter_packages_trie,
remove_beartype_pathhook_unless_packages_trie,
)
from beartype.claw._pkg._clawpkgmake import (
make_conf_hookable,
make_package_names_from_args,
)
from beartype.claw._importlib.clawimppath import (
add_beartype_pathhook,
# remove_beartype_pathhook,
)
from beartype.roar import (
BeartypeClawHookException,
)
from beartype.typing import (
Iterable,
Optional,
)
from beartype._conf.confcls import BeartypeConf
# ....................{ (UN)HOOKERS }....................
#FIXME: Unit test us up, please.
def hook_packages(
# Keyword-only arguments.
*,
# Mandatory keyword-only arguments.
claw_coverage: BeartypeClawCoverage,
conf: BeartypeConf,
# Optional keyword-only arguments.
package_name: Optional[str] = None,
package_names: Optional[Iterable[str]] = None,
) -> None:
'''
Register a new **beartype package import path hook** (i.e., callable
inserted to the front of the standard :mod:`sys.path_hooks` list recursively
applying the :func:`beartype.beartype` decorator to all typed callables and
classes of all submodules of all packages with the passed names on the first
importation of those submodules).
Parameters
----------
claw_coverage : BeartypeClawCoverage
**Import hook coverage** (i.e., competing package scope over which to
apply the path hook added by this function, each with concomitant
tradeoffs with respect to runtime complexity and quality assurance).
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).
package_name : Optional[str]
Either:
* If ``coverage`` is :attr:`BeartypeClawCoverage.PACKAGES_ONE`, the
fully-qualified name of the package to be type-checked.
* Else, ignored.
Defaults to :data:`None`.
package_names : Optional[Iterable[str]]]
Either:
* If ``coverage`` is :attr:`BeartypeClawCoverage.PACKAGES_MANY`, an
iterable of the fully-qualified names of one or more packages to be
type-checked.
* Else, ignored.
Defaults to :data:`None`.
Raises
------
BeartypeClawHookException
If either:
* The passed ``package_names`` parameter is either:
* Neither a string nor an iterable (i.e., fails to satisfy the
:class:`collections.abc.Iterable` protocol).
* An empty string or iterable.
* A non-empty string that is *not* a valid **package name** (i.e.,
``"."``-delimited concatenation of valid Python identifiers).
* A non-empty iterable containing at least one item that is either:
* *Not* a string.
* The empty string.
* A non-empty string that is *not* a valid **package name** (i.e.,
``"."``-delimited concatenation of valid Python identifiers).
* The passed ``conf`` parameter is *not* a beartype configuration (i.e.,
:class:`BeartypeConf` instance).
See Also
--------
https://stackoverflow.com/a/43573798/2809027
StackOverflow answer strongly inspiring the low-level implementation of
this function with respect to inscrutable :mod:`importlib` machinery.
'''
# Avoid circular import dependencies.
from beartype.claw._clawstate import (
claw_lock,
claw_state,
)
# Replace this beartype configuration (which is typically unsuitable for
# usage in import hooks) with a new beartype configuration suitable for
# usage in import hooks.
conf = make_conf_hookable(conf)
# Iterable of the passed fully-qualified names of all packages to be hooked.
package_names = make_package_names_from_args(
claw_coverage=claw_coverage,
conf=conf,
package_name=package_name,
package_names=package_names,
)
# With a submodule-specific thread-safe reentrant lock...
with claw_lock:
# If the caller requested all-packages coverage...
if claw_coverage is BeartypeClawCoverage.PACKAGES_ALL:
# Beartype configuration currently associated with *ALL* packages by
# a prior call to this function if any *OR* "None" (i.e., if this
# function has yet to be called under this Python interpreter).
conf_curr = claw_state.packages_trie.conf_if_hooked
# If the higher-level beartype_all() function (calling this
# lower-level adder) has yet to be called under this interpreter,
# associate this configuration with *ALL* packages.
if conf_curr is None:
claw_state.packages_trie.conf_if_hooked = conf
# Else, beartype_all() was already called under this interpreter.
#
# If the caller passed a different configuration to that prior call
# than that passed to this current call, raise an exception.
elif conf_curr != conf:
raise BeartypeClawHookException(
f'beartype_all() previously passed '
f'conflicting beartype configuration:\n'
f'\t----------( OLD "conf" PARAMETER )----------\n'
f'\t{repr(conf_curr)}\n'
f'\t----------( NEW "conf" PARAMETER )----------\n'
f'\t{repr(conf)}\n'
)
# Else, the caller passed the same configuration to that prior call
# than that passed to the current call.
# Else, the caller requested coverage over a subset of packages. In this
# case...
else:
# For the fully-qualified name of each package to be registered...
for package_name in package_names: # type: ignore[union-attr]
# List of each unqualified basename comprising this name, split
# from this fully-qualified name on "." delimiters. Note that
# the "str.split('.')" and "str.rsplit('.')" calls produce the
# exact same lists under all possible edge cases. We arbitrarily
# call the former rather than the latter for simplicity.
package_basenames = package_name.split('.')
# Current subtrie of the global package trie describing the
# currently iterated basename of this package, initialized to
# the global trie configuring all top-level packages.
subpackages_trie = claw_state.packages_trie
# For each unqualified basename comprising the directed path from
# the root parent package of that package to that package...
for package_basename in package_basenames:
# Current subtrie of that trie describing that parent package if
# that parent package was registered by a prior call to the
# hook_packages() function *OR* "None" (i.e., if that parent
# package has yet to be registered).
subpackages_subtrie = subpackages_trie.get(package_basename)
# If this is the first registration of that parent package,
# register a new subtrie describing that parent package.
#
# Note that this test could be obviated away by refactoring our
# "PackagesTrie" subclass from the "collections.defaultdict"
# superclass rather than the standard "dict" class. Since doing
# so would obscure erroneous attempts to access non-existing
# keys, however, this test is preferable to inviting even *MORE*
# bugs into this bug-riddled codebase. Just kidding! There are
# absolutely no bugs in this codebase. *wink*
if subpackages_subtrie is None:
subpackages_subtrie = \
subpackages_trie[package_basename] = \
PackagesTrie(package_basename=package_basename)
# Else, that parent package was already registered by a prior
# call to this function.
# Iterate the current subtrie one subpackage deeper.
subpackages_trie = subpackages_subtrie
# Since the "package_basenames" list contains at least one basename,
# the above iteration set the currently examined subdictionary
# "subpackages_trie" to at least one subtrie of the global package
# trie. Moreover, that subtrie is guaranteed to describe the current
# (sub)package being registered.
# print(f'Hooked package "{package_name}" subpackage trie {repr(subpackages_trie)}...')
# Beartype configuration currently associated with that package by a
# prior call to this function if any *OR* "None" (i.e., if that
# package has yet to be registered by a prior call to this
# function).
conf_curr = subpackages_trie.conf_if_hooked
# If that package has yet to be registered, associate this
# configuration with that package.
if conf_curr is None:
subpackages_trie.conf_if_hooked = conf
# Else, that package was already registered by a previous call to
# this function.
#
# If the caller passed a different configuration to that prior call
# than that passed to this current call, raise an exception.
elif conf_curr != conf:
raise BeartypeClawHookException(
f'Beartype import hook '
f'(e.g., beartype.claw.beartype_*() function) '
f'previously passed '
f'conflicting beartype configuration for '
f'package name "{package_name}":\n'
f'\t----------( OLD "conf" PARAMETER )----------\n'
f'\t{repr(conf_curr)}\n'
f'\t----------( NEW "conf" PARAMETER )----------\n'
f'\t{repr(conf)}\n'
)
# Else, the caller passed the same configuration to that prior call
# than that passed to the current call. In this case, silently
# ignore this redundant request to reregister that package.
# Lastly, if our beartype import path hook singleton has *NOT* already
# been added to the standard "sys.path_hooks" list, do so now.
#
# Note that we intentionally:
# * Do so in a thread-safe manner *INSIDE* this lock.
# * Defer doing so until *AFTER* the above iteration has successfully
# registered the desired packages with our global trie. Why? This path
# hook subsequently calls the companion get_package_conf_or_none()
# function, which accesses this trie.
add_beartype_pathhook()
#FIXME: Unit test us up, please.
def unhook_packages(
# Keyword-only arguments.
*,
# Mandatory keyword-only arguments.
claw_coverage: BeartypeClawCoverage,
conf: BeartypeConf,
# Optional keyword-only arguments.
package_name: Optional[str] = None,
package_names: Optional[Iterable[str]] = None,
) -> None:
'''
Unregister a previously registered **beartype package import path hook**
(i.e., callable inserted to the front of the standard :mod:`sys.path_hooks`
list recursively applying the :func:`beartype.beartype` decorator to all
typed callables and classes of all submodules of all packages with the
passed names on the first importation of those submodules).
See Also
--------
:func:`.hook_packages`
Further details.
'''
# Avoid circular import dependencies.
from beartype.claw._clawstate import (
claw_lock,
claw_state,
)
# Replace this beartype configuration (which is typically unsuitable for
# usage in import hooks) with a new beartype configuration suitable for
# usage in import hooks.
conf = make_conf_hookable(conf)
# Iterable of the passed fully-qualified names of all packages to be
# unhooked.
package_names = make_package_names_from_args(
claw_coverage=claw_coverage,
conf=conf,
package_name=package_name,
package_names=package_names,
)
# With a submodule-specific thread-safe reentrant lock...
with claw_lock:
# If the caller requested all-packages coverage...
if claw_coverage is BeartypeClawCoverage.PACKAGES_ALL:
# Unhook the beartype configuration previously associated with *ALL*
# packages by a prior call to the beartype_all() function.
claw_state.packages_trie.conf_if_hooked = None
# Else, the caller requested coverage over a subset of packages. In this
# case...
else:
# For the fully-qualified names of each package to be
# unregistered...
for package_name in package_names: # type: ignore[union-attr]
# List of all subpackages tries describing each parent package
# transitively containing the passed package (as well as that of
# that package itself).
subpackages_tries = list(iter_packages_trie(package_name))
# Reverse this list in-place, such that:
# * The first item of this list is the subpackages trie
# describing that package itself.
# * The last item of this list is the subpackages trie
# describing the root package of that package.
subpackages_tries.reverse()
# Unhook the beartype configuration previously associated with
# that package by a prior call to the hook_packages() function.
subpackages_tries[0].conf_if_hooked = None
# Child sub-subpackages trie of the currently iterated
# subpackages trie, describing the child subpackage of the
# current parent package transitively containing that package.
subsubpackages_trie = None
# For each subpackages trie describing a parent package
# transitively containing that package...
for subpackages_trie in subpackages_tries:
# If this is *NOT* the first iteration of this loop (in
# which case this subpackages trie is a parent package
# rather than that package itself) *AND*...
if subsubpackages_trie is not None:
# If this child sub-subpackages trie describing this
# child sub-subpackage has one or more children, then
# this child sub-subpackages trie still stores
# meaningful metadata and is thus *NOT* safely
# deletable. Moreover, this implies that:
# * *ALL* parent subpackages tries of this child
# sub-subpackages trie also still store meaningful
# metadata and are thus also *NOT* safely deletable.
# * There exists no more meaningful work to be performed
# by this iteration. Ergo, we immediately halt this
# iteration now.
if subsubpackages_trie:
break
# Else, this child sub-subpackages trie describing this
# child sub-subpackage has *NO* children, implying this
# child sub-subpackages trie no longer stores any
# meaningful metadata and is thus safely deletable.
# Unqualified basename of this child sub-subpackage.
subsubpackage_basename = (
subsubpackages_trie.package_basename)
# Delete this child sub-subpackages trie from this
# parent subpackages trie.
del subpackages_trie[subsubpackage_basename] # pyright: ignore
# Else, this is the first iteration of this loop.
# Treat this parent subpackages trie as the child
# sub-subpackages trie in the next iteration of this loop.
subsubpackages_trie = subpackages_trie
# Lastly, if *ALL* meaningful metadata has now been removed from our
# global trie, remove our beartype import path hook singleton from the
# standard "sys.path_hooks" list.
#
# Note that we intentionally:
# * Do so in a thread-safe manner *INSIDE* this lock.
# * Defer doing so until *AFTER* the above iteration has successfully
# unregistered the desired packages with our global trie.
remove_beartype_pathhook_unless_packages_trie()