1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
|
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2025 Beartype authors.
# See "LICENSE" for further details.
'''
Project-wide **Python module tester** (i.e., callables dynamically testing
modules and/or attributes in modules) utilities.
This private submodule is *not* intended for importation by downstream callers.
'''
# ....................{ IMPORTS }....................
from beartype.roar._roarexc import _BeartypeUtilModuleException
from beartype.typing import Optional
from beartype._cave._cavefast import ModuleType
from beartype._data.typing.datatyping import TypeException
from beartype._util.error.utilerrwarn import warnings_ignored
from beartype._util.text.utiltextidentifier import die_unless_identifier
from beartype._util.text.utiltextversion import convert_str_version_to_tuple
from importlib.metadata import version as get_module_version # type: ignore[attr-defined]
# ....................{ RAISERS }....................
#FIXME: Excise us up. This function is no longer called anywhere. *sigh*
def die_unless_module_attr_name(
# Mandatory parameters.
module_attr_name: str,
# Optional parameters.
exception_cls: TypeException = _BeartypeUtilModuleException,
exception_prefix: str = 'Module attribute name ',
) -> None:
'''
Raise an exception unless the passed string is the fully-qualified
syntactically valid name of a **module attribute** (i.e., object declared
at module scope by a module) that may or may not actually exist.
This validator does *not* validate this attribute to actually exist -- only
that the name of this attribute is syntactically valid.
Parameters
----------
module_attr_name : str
Fully-qualified name of the module attribute to be validated.
exception_cls : type, optional
Type of exception to be raised in the event of a fatal error. Defaults
to :class:`._BeartypeUtilModuleException`.
exception_prefix : str, optional
Human-readable label prefixing the representation of this object in the
exception message. Defaults to something reasonably sane.
Raises
------
exception_cls
If either:
* This name is *not* a string.
* This name is a string containing either:
* *No* ``.`` characters and thus either:
* Is relative to the calling subpackage and thus *not*
fully-qualified (e.g., ``muh_submodule``).
* Refers to a builtin object (e.g., ``str``). While technically
fully-qualified, the names of builtin objects are *not*
explicitly importable as is. Since these builtin objects are
implicitly imported everywhere, there exists *no* demonstrable
reason to even attempt to import them anywhere.
* One or more ``.`` characters but syntactically invalid as an
identifier (e.g., ``0h!muh?G0d.``).
'''
assert isinstance(exception_cls, type), f'{repr(exception_cls)} not type.'
assert isinstance(exception_prefix, str), (
f'{repr(exception_prefix)} not string.')
# If this object is *NOT* a string, raise an exception.
if not isinstance(module_attr_name, str):
raise exception_cls(
f'{exception_prefix}{repr(module_attr_name)} not string.')
# Else, this object is a string.
#
# If this string contains *NO* "." characters and thus either is relative to
# the calling subpackage or refers to a builtin object, raise an exception.
elif '.' not in module_attr_name:
raise exception_cls(
f'{exception_prefix}"{module_attr_name}" '
f'relative or refers to builtin object '
f'(i.e., due to containing no "." characters).'
)
# Else, this string contains one or more "." characters and is thus the
# fully-qualified name of a non-builtin type.
#
# If this string is syntactically invalid as a fully-qualified module
# attribute name, raise an exception.
else:
die_unless_identifier(
text=module_attr_name,
exception_cls=exception_cls,
exception_prefix=exception_prefix,
)
# Else, this string is syntactically valid as a fully-qualified module
# attribute name.
# ....................{ TESTERS }....................
def is_module(
# Mandatory parameters.
module_name: str,
# Optional parameters.
is_warnings_ignore: bool = False,
) -> bool:
'''
:data:`True` only if the module or C extension with the passed
fully-qualified name is importable under the active Python interpreter.
Caveats
-------
**This tester dynamically imports this module as an unavoidable side effect
of performing this test.**
Parameters
----------
module_name : str
Fully-qualified name of the module to be imported.
is_warnings_ignore : bool, optional
:data:`True` only if this tester ignores *all* warnings transitively
emitted as a side effect by the importation of this module. Defaults to
:data:`False` for safety.
Returns
-------
bool
:data:`True` only if this module is importable.
Warns
-----
BeartypeModuleUnimportableWarning
If a module with this name exists *but* that module is unimportable due
to raising module-scoped exceptions at importation time.
'''
# Avoid circular import dependencies.
from beartype._util.module.utilmodimport import import_module_or_none
# Module with this name if this module is importable *OR* "None" otherwise.
module: Optional[ModuleType] = None
# If ignoring *ALL* warnings transitively emitted as a side effect by the
# importation of this module, attempt to dynamically import this module
# under a context manager ignoring these warnings.
if is_warnings_ignore:
with warnings_ignored():
module = import_module_or_none(module_name)
# Else, dynamically import this module *WITHOUT* ignoring these warnings.
else:
module = import_module_or_none(module_name)
# Return true only if this module is importable.
return module is not None
#FIXME: Unit test us up against "setuptools", the only third-party package
#*BASICALLY* guaranteed to be importable.
def is_module_version_at_least(module_name: str, version_minimum: str) -> bool:
'''
:data:`True` only if the module or C extension with the passed
fully-qualified name is both importable under the active Python interpreter
*and* at least as new as the passed version.
Caveats
-------
**This tester dynamically imports this module as an unavoidable side effect
of performing this test.**
Parameters
----------
module_name : str
Fully-qualified name of the module to be imported.
version_minimum : str
Minimum version to test this module against as a dot-delimited
:pep:`440`-compliant version specifier (e.g., ``42.42.42rc42.post42``).
Returns
-------
bool
:data:`True` only if:
* This module is importable.
* This module's version is at least the passed version.
Warns
-----
BeartypeModuleUnimportableWarning
If a module with this name exists *but* that module is unimportable due
to raising module-scoped exceptions at importation time.
'''
assert isinstance(version_minimum, str), (
f'{repr(version_minimum)} not string.')
# If this module is unimportable, return false immediately.
if not is_module(module_name):
return False
# Else, this module is importable.
# Current version of this module installed under the active Python
# interpreter if any *OR* raise an exception otherwise (which should
# *NEVER* happen by prior logic testing this module to be importable).
version_actual = get_module_version(module_name)
# Tuples of version parts parsed from version strings.
version_actual_parts = convert_str_version_to_tuple(version_actual)
version_minimum_parts = convert_str_version_to_tuple(version_minimum)
# Return true only if this module's version satisfies this minimum.
return version_actual_parts >= version_minimum_parts
# ....................{ TESTERS ~ package }....................
#FIXME: Unit test us up, please.
def is_package(package_name: str, **kwargs) -> bool:
'''
:data:`True` only if the package with the passed fully-qualified name is
importable under the active Python interpreter.
Caveats
-------
**This tester dynamically imports this module as an unavoidable side effect
of performing this test.**
Parameters
----------
package_name : str
Fully-qualified name of the package to be imported.
All remaining keyword parameters are passed as is to the lower-level
:func:`.is_module` tester.
Returns
-------
bool
:data:`True` only if this package is importable.
Warns
-----
BeartypeModuleUnimportableWarning
If a package with this name exists *but* that package is unimportable
due to raising module-scoped exceptions from the top-level ``__init__``
submodule of this package at importation time.
'''
# Be the one liner you want to see in the world.
return is_module(f'{package_name}.__init__', **kwargs)
|