#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.
'''
**Beartype import hooks** (i.e., public-facing functions integrating high-level
:mod:`importlib` machinery required to implement :pep:`302`- and
:pep:`451`-compliant import hooks with the abstract syntax tree (AST)
transformations defined by the low-level :mod:`beartype.claw._ast.clawastmain`
submodule).
This private submodule is the main entry point for this subpackage. Nonetheless,
this private submodule is *not* intended for importation by downstream callers.
'''
# ....................{ TODO }....................
#FIXME: Improve the beartype_package() and beartype_packages() functions to emit
#non-fatal warnings when the passed package or packages have already been
#imported (i.e., are in the "sys.modules" list).
# ....................{ IMPORTS }....................
from beartype.roar import BeartypeClawHookUnpackagedException
from beartype.claw._pkg.clawpkgenum import BeartypeClawCoverage
from beartype.claw._pkg.clawpkghook import hook_packages
from beartype.typing import Iterable
from beartype._cave._cavefast import CallableFrameType
from beartype._conf.confcls import (
BEARTYPE_CONF_DEFAULT,
BeartypeConf,
)
from beartype._data.module.datamodpy import SCRIPT_MODULE_NAME
from beartype._util.func.utilfuncfile import get_func_filename_or_none
from beartype._util.func.utilfuncframe import (
get_frame,
get_frame_module_name,
get_frame_package_name,
)
from pathlib import PurePath
# ....................{ HOOKERS }....................
[docs]def beartype_all(
# Optional keyword-only parameters.
*,
conf: BeartypeConf = BEARTYPE_CONF_DEFAULT,
) -> None:
'''
Register 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* annotated callables, classes, and variable assignments
across *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).
This function is the runtime equivalent of a full-blown static type checker
like ``mypy`` or ``pyright``, enabling full-stack runtime type-checking of
the current app -- including submodules defined by both:
* First-party proprietary packages directly authored for this app.
* Third-party open-source packages authored and maintained elsewhere.
This function is thread-safe.
Usage
-----
This function is intended to be called from module scope as the first
statement of the top-level ``__init__`` submodule of the top-level package
of an app to be fully type-checked by :mod:`beartype`. This function then
registers an import path hook type-checking *all* annotated callables,
classes, and variable assignments across *all* submodules of *all* packages
on the first importation of those submodules: e.g.,
.. code-block:: python
# At the very top of "muh_package.__init__":
from beartype.claw import beartype_all
beartype_all() # <-- beartype all subsequent imports, yo
# Import submodules *AFTER* calling beartype_all().
from muh_package._some_module import muh_function # <-- @beartype it!
from yer_package.other_module import muh_class # <-- @beartype it!
Caveats
-------
**This function is not intended to be called from intermediary APIs,
libraries, frameworks, or other middleware.** This function is *only*
intended to be called from full stack end-user applications as a convenient
alternative to manually passing the names of all packages to be type-checked
to the more granular :func:`.beartype_packages` function. This function
imposes runtime type-checking on downstream reverse dependencies that may
not necessarily want, expect, or tolerate runtime type-checking. This
function should typically *only* be called by proprietary packages not
expected to be reused by others. Open-source packages are advised to call
other functions instead.
**tl;dr:** *Only call this function in non-reusable end-user apps.*
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.
Raises
------
BeartypeClawHookException
If the passed ``conf`` parameter is *not* a beartype configuration
(i.e., :class:`BeartypeConf` instance).
'''
# The advantage of one-liners is the vantage of vanity.
hook_packages(claw_coverage=BeartypeClawCoverage.PACKAGES_ALL, conf=conf)
[docs]def beartype_this_package(
# Optional keyword-only parameters.
*,
conf: BeartypeConf = BEARTYPE_CONF_DEFAULT,
) -> None:
'''
Register a new **current package beartype 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*
annotated callables, classes, and variable assignments across *all*
submodules of the current user-defined package calling this function on the
first importation of those submodules).
This function is thread-safe.
Usage
-----
This function is intended to be called from module scope as the first
statement of the top-level ``__init__`` submodule of any package to be
type-checked by :mod:`beartype`. This function then registers an import path
hook type-checking *all* annotated callables, classes, and variable
assignments across *all* submodules of that package on the first importation
of those submodules: e.g.,
.. code-block:: python
# At the very top of "muh_package.__init__":
from beartype.claw import beartype_this_package
beartype_this_package() # <-- beartype all subsequent imports, yo
# Import package submodules *AFTER* calling beartype_this_package().
from muh_package._some_module import muh_function # <-- @beartype it!
from muh_package.other_module import muh_class # <-- @beartype it!
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.
Raises
------
BeartypeClawHookException
If the passed ``conf`` parameter is *not* a beartype configuration
(i.e., :class:`.BeartypeConf` instance).
BeartypeClawHookUnpackagedException
If this function is called from outside any package structure (e.g.,
top-level module or executable script).
'''
# Stack frame encapsulating the user-defined lexical scope directly calling
# this import hook.
#
# Note that:
# * This call is guaranteed to succeed without error. Why? Because:
# * The current call stack *ALWAYS* contains at least one stack frame.
# Ergo, get_frame(0) *ALWAYS* succeeds without error.
# * The call to this import hook guaranteeably adds yet another stack
# frame to the current call stack. Ergo, get_frame(1) also *ALWAYS*
# succeeds without error in this context.
# * This and the following logic *CANNOT* reasonably be isolated to a new
# private helper function. Why? Because this logic itself calls existing
# private helper functions assuming the caller to be at the expected
# position on the current call stack.
frame_caller: CallableFrameType = get_frame(1) # type: ignore[assignment,misc]
# Fully-qualified name of the parent package of the child module defining
# that caller if that module resides in some package *OR* the empty string
# otherwise (i.e., if that module is a top-level module or script residing
# outside any package structure).
frame_package_name = get_frame_package_name(frame_caller)
# print(f'beartype_this_package: {frame_caller_package_name}')
# print(f'beartype_this_package: {repr(frame_caller)}')
#FIXME: Is "pragma: no cover" accurate here? Is this condition untestable?
# If that module has *NO* parent package, raise an exception. Why? Because
# this function uselessly (but silently) reduces to a noop when called from
# a top-level module or script residing outside any package. Why? Because
# this function installs an import hook applicable only to subsequently
# imported submodules of the current package. By definition, a top-level
# module or script has *NO* package and thus *NO* sibling submodules and
# thus *NO* meaningful imports to be hooked. To avoid unwanted confusion, we
# intentionally notify the user with a loud exception.
if not frame_package_name: # pragma: no cover
# Exception message to be raised below.
exception_message: str = None # type: ignore[assignment]
# Fully-qualified name of the module encapsulating the caller.
frame_module_name = get_frame_module_name(
frame=frame_caller,
exception_cls=BeartypeClawHookUnpackagedException,
)
# If the caller is a script rather than a module, this name is the
# useless magic string "__main__". In this case...
if frame_module_name == SCRIPT_MODULE_NAME:
# Absolute filename of this script if this script physically resides
# on the local filesystem *OR* "None" otherwise (i.e., if this
# script is dynamically defined in-memory).
frame_filename = get_func_filename_or_none(frame_caller)
# If this script physically exists...
if frame_filename:
# Prefix this message appropriately.
exception_message_prefix = (
f'Top-level script "{frame_filename}" ')
# Else, this script only exists in memory. In this case...
else:
# Prefix this message appropriately.
exception_message_prefix = 'In-memory script '
# Fabricate an arbitrary filename. Just do it!
frame_filename = 'scripts/main.py'
# Path object encapsulating this filename.
frame_path = PurePath(frame_filename)
# Basename of the parent directory containing this script, defined
# as either...
frame_package_basename = (
# If this filename contains at least two basenames, then:
# * The last basename is that of this script.
# * The second-to-last basename is that of the parent directory
# containing this script.
frame_path.parts[-2]
if len(frame_path.parts) >= 2 else
# Else, this filename contains only the basename of this script.
# In this case, fabricate an arbitrary basename. Just do it!
'scripts'
)
# Exception message to be raised below.
exception_message = (
f'{exception_message_prefix}resides outside package structure. '
f'Consider calling another "beartype.claw" import hook. '
f'However, note that only other modules will be type-checked. '
f'"{frame_filename}" itself will remain unchecked. '
f'All business logic should reside in submodules '
f'subsequently imported by "{frame_filename}": e.g.,\n'
f' # Instead of this at the top of "{frame_filename}"...\n'
f' from beartype.claw import beartype_this_package # <-- you are here\n'
f' beartype_this_package() # <-- feels bad\n'
f'\n'
f' # ...pass the basename of the "{frame_package_basename}/" subdirectory explicitly.\n'
f' from beartype.claw import beartype_package # <-- you want to be here\n'
f' beartype_package("{frame_package_basename}") # <-- feels good\n'
f'\n'
f' from {frame_package_basename}.main_submodule import main_func # <-- still feels good\n'
f' main_func() # <-- *GOOD*! "beartype.claw" type-checks this\n'
f' some_global: str = 0xFEEDFACE # <-- *BAD*! "beartype.claw" ignores this\n'
f'This has been a message from your friendly neighbourhood bear.'
)
# Else, the caller is a module with a useful name. In this case, define
# an exception message.
#
# Note that this edge case implies that this is a top-level module
# residing outside a package that was *NOT* run as a script. Since this
# should *BASICALLY* never occur, there isn't terribly much we can do.
else:
exception_message = (
f'Top-level module "{frame_module_name}" '
f'resides outside package structure but was '
f'*NOT* directly run as a script. '
f'"beartype.claw" import hooks require that modules either '
f'reside inside a package structure or be '
f'directly run as scripts. '
f'Since neither applies here, you are now off the deep end. '
f'@beartype no longer has any idea what is going on, sadly. '
f'Consider directly decorating classes and functions by the '
f'@beartype.beartype decorator instead: e.g.,\n'
f' # Instead of this at the top of "{frame_module_name}"...\n'
f' from beartype.claw import beartype_this_package # <-- you are here\n'
f' beartype_this_package() # <-- feels bad\n'
f'\n'
f" # ...go old-school like it's 2017 and you just don't care.\n"
f' from beartype import beartype # <-- you want to be here\n'
f' @beartype # <-- feels good, yet kinda icky at same time\n'
f' def spicy_func() -> str: ... # <-- *GOOD*! @beartype type-checks this\n'
f' some_global: str = 0xFEEDFACE # <-- *BAD*! @beartype ignores this, but what can you do\n'
f'For your safety, @beartype will now crash and burn.'
)
# Raise an exception.
raise BeartypeClawHookUnpackagedException(exception_message)
# Else, that module has a parent package.
# Add a new import path hook beartyping this package.
hook_packages(
claw_coverage=BeartypeClawCoverage.PACKAGES_ONE,
package_name=frame_package_name,
conf=conf,
)
#FIXME: Add a "Usage" docstring section resembling that of the docstring for the
#beartype_this_package() function.
[docs]def beartype_package(
# Mandatory parameters.
package_name: str,
# Optional keyword-only parameters.
*,
conf: BeartypeConf = BEARTYPE_CONF_DEFAULT,
) -> None:
'''
Register a new **single package beartype 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* annotated
callables, classes, and variable assignments across *all* submodules of the
package with the passed names on the first importation of those submodules).
This function is thread-safe.
Parameters
----------
package_name : str
Fully-qualified name of the package to be type-checked.
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.
Raises
------
BeartypeClawHookException
If either:
* The passed ``conf`` parameter is *not* a beartype configuration (i.e.,
:class:`BeartypeConf` instance).
* The passed ``package_name`` parameter 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).
'''
# Add a new import path hook beartyping this package.
hook_packages(
claw_coverage=BeartypeClawCoverage.PACKAGES_ONE,
package_name=package_name,
conf=conf,
)
#FIXME: Add a "Usage" docstring section resembling that of the docstring for the
#beartype_this_package() function.
[docs]def beartype_packages(
# Mandatory parameters.
package_names: Iterable[str],
# Optional keyword-only parameters.
*,
conf: BeartypeConf = BEARTYPE_CONF_DEFAULT,
) -> None:
'''
Register a new **multiple package beartype 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*
annotated callables, classes, and variable assignments across *all*
submodules of all packages with the passed names on the first importation of
those submodules).
This function is thread-safe.
Parameters
----------
package_names : Iterable[str]
Iterable of the fully-qualified names of one or more packages to be
type-checked.
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.
Raises
------
BeartypeClawHookException
If either:
* The passed ``conf`` parameter is *not* a beartype configuration (i.e.,
:class:`BeartypeConf` instance).
* The passed ``package_names`` parameter is either:
* Non-iterable (i.e., fails to satisfy the
:class:`collections.abc.Iterable` protocol).
* An empty iterable.
* 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).
'''
# Add a new import path hook beartyping these packages.
hook_packages(
claw_coverage=BeartypeClawCoverage.PACKAGES_MANY,
package_names=package_names,
conf=conf,
)