Source code for beartype.claw._clawmain

#!/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, )