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.
'''
Project-wide **typing module** utilities (i.e., callables dynamically testing
and importing attributes declared at module scope by either the standard
:mod:`typing` or third-party :mod:`typing_extensions` modules).
This private submodule is *not* intended for importation by downstream callers.
'''
# ....................{ IMPORTS }....................
from beartype.roar import BeartypeModuleAttributeNotFoundWarning
from beartype.roar._roarexc import _BeartypeUtilModuleException
from beartype.typing import (
Any,
Iterable,
Union,
)
from beartype._data.hint.datahinttyping import TypeException
from beartype._data.module.datamodtyping import TYPING_MODULE_NAMES
from beartype._util.cache.utilcachecall import callable_cached
from beartype._util.error.utilerrwarn import issue_warning
from beartype._util.module.utilmodimport import import_module_attr_or_none
from collections.abc import Iterable as IterableABC
# ....................{ TESTERS }....................
#FIXME: Unit test us up, please.
def is_typing_attr(
# Mandatory parameters.
typing_attr_basename: str,
# Optional parameters.
exception_cls: TypeException = _BeartypeUtilModuleException,
) -> bool:
'''
:data:`True` only if a **typing attribute** (i.e., object declared at module
scope by either the :mod:`typing` or :mod:`typing_extensions` modules) with
the passed unqualified name is importable from one or more of these
modules.
This function is effectively memoized for efficiency.
Parameters
----------
typing_attr_basename : str
Unqualified name of the attribute to be imported from a typing module.
Returns
-------
bool
:data:`True` only if the :mod:`typing` or :mod:`typing_extensions`
modules declare an attribute with this name.
exception_cls : Type[Exception]
Type of exception to be raised by this function. Defaults to
:class:`._BeartypeUtilModuleException`.
Raises
------
exception_cls
If this name is syntactically invalid.
Warns
-----
BeartypeModuleUnimportableWarning
If any of these modules raise module-scoped exceptions at importation
time. That said, the :mod:`typing` and :mod:`typing_extensions` modules
are scrupulously tested and thus unlikely to raise such exceptions.
'''
# Return true only if an attribute with this name is importable from either
# the "typing" *OR* "typing_extensions" modules.
#
# Note that positional rather than keyword arguments are intentionally
# passed to optimize memoization efficiency.
return import_typing_attr_or_none(
typing_attr_basename, exception_cls) is not None
# ....................{ IMPORTERS }....................
def import_typing_attr(
# Mandatory parameters.
typing_attr_basename: str,
# Optional parameters.
exception_cls: TypeException = _BeartypeUtilModuleException,
) -> Any:
'''
Dynamically import and return the **typing attribute** (i.e., object
declared at module scope by either the :mod:`typing` or
:mod:`typing_extensions` modules) with the passed unqualified name if
importable from one or more of these modules *or* raise an exception
otherwise (i.e., if this attribute is *not* importable from these modules).
This function is effectively memoized for efficiency.
Parameters
----------
typing_attr_basename : str
Unqualified name of the attribute to be imported from a typing module.
exception_cls : Type[Exception]
Type of exception to be raised by this function. Defaults to
:class:`._BeartypeUtilModuleException`.
Returns
-------
object
Attribute with this name dynamically imported from a typing module.
Raises
------
exception_cls
If either:
* This name is syntactically invalid.
* Neither the :mod:`typing` nor :mod:`typing_extensions` modules
declare an attribute with this name.
Warns
-----
BeartypeModuleUnimportableWarning
If any of these modules raise module-scoped exceptions at importation
time. That said, the :mod:`typing` and :mod:`typing_extensions` modules
are scrupulously tested and thus unlikely to raise such exceptions.
See Also
--------
:func:`beartype._util.module.utilmodimport.import_module_typing_any_attr_or_none`
Further details.
'''
# Avoid circular import dependencies.
from beartype._util.module.utilmodtest import is_module
# Attribute with this name imported from either the "typing" or
# "typing_extensions" modules if one or more of these modules declare this
# attribute *OR* "None" otherwise.
#
# Note that positional rather than keyword arguments are intentionally
# passed to optimize memoization efficiency.
typing_attr = import_typing_attr_or_none(
typing_attr_basename, exception_cls)
# If none of these modules declare this attribute...
if typing_attr is None:
# Substrings prefixing and suffixing exception messages raised below.
EXCEPTION_PREFIX = (
f'Typing attributes "typing.{typing_attr_basename}" and '
f'"typing_extensions.{typing_attr_basename}" not found. '
)
EXCEPTION_SUFFIX = (
'We apologize for the inconvenience and hope you had a '
'great dev cycle flying with Air Beartype, '
'"Your Grizzled Pal in the Friendly Skies."'
)
# If the "typing_extensions" module is importable, raise an
# appropriate exception.
if is_module('typing_extensions'):
raise exception_cls(
f'{EXCEPTION_PREFIX} Please either '
f'(A) update the "typing_extensions" package or '
f'(B) update to a newer Python version. {EXCEPTION_SUFFIX}'
)
# Else, the "typing_extensions" module is unimportable. In this
# case, raise an appropriate exception.
else:
raise exception_cls(
f'{EXCEPTION_PREFIX} Please either '
f'(A) install the "typing_extensions" package or '
f'(B) update to a newer Python version. {EXCEPTION_SUFFIX}'
)
# Else, one or more of these modules declare this attribute.
# Return this attribute.
return typing_attr
#FIXME: Unit test us up, please.
def import_typing_attr_or_none(
# Mandatory parameters.
typing_attr_basename: str,
# Optional parameters.
exception_cls: TypeException = _BeartypeUtilModuleException,
) -> Any:
'''
Dynamically import and return the **typing attribute** (i.e., object
declared at module scope by either the :mod:`typing` or
:mod:`typing_extensions` modules) with the passed unqualified name if
importable from one or more of these modules *or* :data:`None` otherwise
otherwise (i.e., if this attribute is *not* importable from these modules).
This function is effectively memoized for efficiency.
Parameters
----------
typing_attr_basename : str
Unqualified name of the attribute to be imported from a typing module.
exception_cls : Type[Exception]
Type of exception to be raised by this function. Defaults to
:class:`._BeartypeUtilModuleException`.
Returns
-------
object
Attribute with this name dynamically imported from a typing module.
Raises
------
exception_cls
If this name is syntactically invalid.
Warns
-----
BeartypeModuleUnimportableWarning
If any of these modules raise module-scoped exceptions at importation
time. That said, the :mod:`typing` and :mod:`typing_extensions` modules
are scrupulously tested and thus unlikely to raise exceptions.
See Also
--------
:func:`import_typing_attr_or_fallback`
Further details.
'''
# One-liners in the rear view mirror may be closer than they appear.
#
# Note that parameters are intentionally passed positionally rather than by
# keyword for memoization efficiency.
return import_typing_attr_or_fallback(
typing_attr_basename, None, exception_cls)
#FIXME: Unit test us up, please.
#FIXME: Leverage above, please.
@callable_cached
def import_typing_attr_or_fallback(
# Mandatory parameters.
typing_attr_basename: str,
fallback: object,
# Optional parameters.
exception_cls: TypeException = _BeartypeUtilModuleException,
) -> Any:
'''
Dynamically import and return the **typing attribute** (i.e., object
declared at module scope by either the :mod:`typing` or
:mod:`typing_extensions` modules) with the passed unqualified name if
importable from one or more of these modules *or* the passed fallback
otherwise otherwise (i.e., if this attribute is *not* importable from these
modules).
Specifically, this function (in order):
#. If the official :mod:`typing` module bundled with the active Python
interpreter declares that attribute, dynamically imports and returns
that attribute from that module.
#. Else if the third-party (albeit quasi-official) :mod:`typing_extensions`
module requiring external installation under the active Python
interpreter declares that attribute, dynamically imports and returns
that attribute from that module.
#. Else, returns the passed fallback value.
This function is memoized for efficiency.
Parameters
----------
typing_attr_basename : str
Unqualified name of the attribute to be imported from a typing module.
fallback : object
Arbitrary value to be returned as a last-ditch fallback if *no* typing
module declares this attribute.
exception_cls : Type[Exception]
Type of exception to be raised by this function. Defaults to
:class:`._BeartypeUtilModuleException`.
Returns
-------
object
Attribute with this name dynamically imported from a typing module.
Raises
------
exception_cls
If this name is syntactically invalid.
Warns
-----
BeartypeModuleUnimportableWarning
If any of these modules raise module-scoped exceptions at importation
time. That said, the :mod:`typing` and :mod:`typing_extensions` modules
are scrupulously tested and thus unlikely to raise exceptions.
'''
# Attribute with this name imported from the "typing" module if that module
# declares this attribute *OR* "None" otherwise.
typing_attr = import_module_attr_or_none(
attr_name=f'typing.{typing_attr_basename}',
exception_cls=exception_cls,
exception_prefix='Typing attribute ',
)
# If the "typing" module does *NOT* declare this attribute...
if typing_attr is None:
# Attribute with this name imported from the "typing_extensions" module
# if that module declares this attribute *OR* "None" otherwise.
typing_attr = import_module_attr_or_none(
attr_name=f'typing_extensions.{typing_attr_basename}',
exception_cls=exception_cls,
exception_prefix='Typing attribute ',
)
# If the "typing_extensions" module also does *NOT* declare this
# attribute, fallback to the passed fallback value.
if typing_attr is None:
typing_attr = fallback
# Else, the "typing_extensions" module declares this attribute.
# Else, the "typing" module declares this attribute.
# Return either this attribute if one or more of these modules declare this
# attribute *OR* this fallback otherwise.
return typing_attr
# ....................{ GETTERS }....................
#FIXME: Unit test us up, please.
@callable_cached
def get_typing_attrs(typing_attr_basename: str) -> frozenset:
'''
Frozen set of all attributes with the passed unqualified basename declared
by all importable typing modules, silently ignoring those modules failing to
declare this attribute.
This getter intentionally returns a set rather than a list. Why? Duplicates.
The third-party :mod:`typing_extensions` module duplicates *all* type hint
factories implemented by the standard :mod:`typing` module under the most
recently released version of Python.
This getter is memoized for efficiency.
Attributes
----------
typing_attr_basename : str
Unqualified name of the attribute to be dynamically imported from
each typing module.
Yields
------
set
Set of all attributes with the passed unqualified basename declared by
all importable typing modules.
'''
assert isinstance(typing_attr_basename, str), (
f'{repr(typing_attr_basename)} not string.')
# Set of all importable attributes to be returned by this getter.
typing_attrs: set = set()
# For the fully-qualified name of each quasi-standard typing module...
for typing_module_name in TYPING_MODULE_NAMES:
# Attribute with this name dynamically imported from that module if
# that module defines this attribute *OR* "None" otherwise.
typing_attr = import_module_attr_or_none(
f'{typing_module_name}.{typing_attr_basename}')
# If that module fails to define this attribute, silently continue to
# the next module.
if typing_attr is None:
continue
# Else, that module declares this attribute.
# Append this attribute to this list.
typing_attrs.add(typing_attr)
# Return this set, coerced into a frozen set for caching purposes.
return frozenset(typing_attrs)
# ....................{ ITERATORS }....................
#FIXME: Replace *ALL* calls to this by calls to get_typing_attrs(), please.
#FIXME: Currently unused but preserved for posterity. Consider excising, please.
def iter_typing_attrs(
# Mandatory parameters.
typing_attr_basenames: Union[str, Iterable[str]],
# Optional parameters.
is_warn: bool = False,
typing_module_names: Iterable[str] = TYPING_MODULE_NAMES,
) -> IterableABC:
'''
Generator iteratively yielding all attributes with the passed basename
declared by the quasi-standard typing modules with the passed
fully-qualified names, silently ignoring those modules failing to declare
such an attribute.
Attributes
----------
typing_attr_basenames : Union[str, Iterable[str]]
Either:
* Unqualified name of the attribute to be dynamically imported from
each typing module, in which case either:
* If the currently iterated typing module defines this attribute,
this generator yields this attribute imported from that module.
* Else, this generator silently ignores that module.
* Iterable of one or more such names, in which case either:
* If the currently iterated typing module defines *all* attributes,
this generator yields a tuple whose items are these attributes
imported from that module (in the same order).
* Else, this generator silently ignores that module.
is_warn : bool
:data:`True` only if emitting non-fatal warnings for typing modules
failing to define all passed attributes. If ``typing_module_names`` is
passed, this parameter should typically also be passed as :data:`True`
for safety. Defaults to :data:`False`.
typing_module_names: Iterable[str]
Iterable of the fully-qualified names of all typing modules to
dynamically import this attribute from. Defaults to
:data:`.TYPING_MODULE_NAMES`.
Yields
------
Union[object, Tuple[object, ...]]
Either:
* If passed only an attribute basename, the attribute with that
basename declared by each typing module.
* If passed an iterable of one or more attribute basenames, a tuple
whose items are the attributes with those basenames (in the same
order) declared by each typing module.
'''
assert isinstance(is_warn, bool), f'{is_warn} not boolean.'
assert isinstance(typing_attr_basenames, (str, IterableABC)), (
f'{typing_attr_basenames} not string.')
assert typing_attr_basenames, '"typing_attr_basenames" empty.'
assert isinstance(typing_module_names, IterableABC), (
f'{repr(typing_module_names)} not iterable.')
assert typing_module_names, '"typing_module_names" empty.'
assert all(
isinstance(typing_module_name, str)
for typing_module_name in typing_module_names
), f'One or more {typing_module_names} items not strings.'
# If passed an attribute basename, pack this into a tuple containing only
# this basename for ease of use.
if isinstance(typing_attr_basenames, str):
typing_attr_basenames = (typing_attr_basenames,)
# Else, an iterable of attribute basenames was passed. In this case...
else:
assert all(
isinstance(typing_attr_basename, str)
for typing_attr_basename in typing_attr_basenames
), f'One or more {typing_attr_basenames} items not strings.'
# In either case, this parameter is now a tuple of attribute basenames.
# List of all imported attributes to be yielded from each iteration of the
# generator implicitly returned by this generator function.
typing_attrs: list = []
# For the fully-qualified name of each quasi-standard typing module...
for typing_module_name in typing_module_names:
# Clear this list *BEFORE* appending to this list below.
typing_attrs.clear()
# For the basename of each attribute to be imported from that module...
for typing_attr_basename in typing_attr_basenames:
# Fully-qualified name of this attribute declared by that module.
typing_attr_name = f'{typing_module_name}.{typing_attr_basename}'
# Attribute with this name dynamically imported from that module if
# that module defines this attribute *OR* "None" otherwise.
typing_attr = import_module_attr_or_none(typing_attr_name)
# If that module fails to define this attribute...
if typing_attr is None:
# If emitting non-fatal warnings, do so.
if is_warn:
issue_warning(
cls=BeartypeModuleAttributeNotFoundWarning,
message=(
f'Ignoring undefined typing attribute '
f'"{typing_attr_name}"...'
),
)
# Else, silently reduce to a noop.
# Continue to the next module.
break
# Else, that module declares this attribute.
# Append this attribute to this list.
typing_attrs.append(typing_attr)
# If that module declares *ALL* attributes...
else:
# If exactly one attribute name was passed, yield this attribute as
# is (*WITHOUT* packing this attribute into a tuple).
if len(typing_attrs) == 1:
yield typing_attrs[0]
# Else, two or more attribute names were passed. In this case, yield
# these attributes as a tuple.
else:
yield tuple(typing_attrs)
# Else, that module failed to declare one or more attributes. In this
# case, silently continue to the next module.