Skip to content

Commit

Permalink
Test suite type hint metadata fixturization x 1.
Browse files Browse the repository at this point in the history
This commit is the first in a commit chain fundamentally refactoring our
test suite to leverage space- and time-efficient `pytest` session-scoped
fixtures rather than space- and time-inefficient ad-hoc machinery
previously defined by the `beartype_test.a00_unit.data.hint.pep`
subpackage, which @leycec requires to preserve personal sanity while
maintaining this lumbering juggernaut but nobody else particularly cares
about. It's best *not* to ask what this is about. Just know that our
test suite is demonstrably improving into something maintainable that
will no longer destroy @leycec's precious sanity points. (*Cruddy mud!*)
  • Loading branch information
leycec committed Oct 27, 2023
1 parent a8fe768 commit 69e2fe4
Show file tree
Hide file tree
Showing 7 changed files with 740 additions and 342 deletions.
72 changes: 66 additions & 6 deletions beartype/_util/module/lib/utiltyping.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,67 @@ def import_typing_attr_or_fallback(
# 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:
# Fully-qualified name of this attribute declared by that module.
module_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(
module_attr_name=module_attr_name,
exception_prefix=f'"{typing_module_name}" attribute ',
)

# 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]],
Expand Down Expand Up @@ -363,11 +423,11 @@ def iter_typing_attrs(
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`.
:data:`.TYPING_MODULE_NAMES`.
Yields
------
Union[object, Tuple[object]]
Union[object, Tuple[object, ...]]
Either:
* If passed only an attribute basename, the attribute with that
Expand Down Expand Up @@ -440,12 +500,12 @@ def iter_typing_attrs(
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 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, 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
Expand Down
43 changes: 5 additions & 38 deletions beartype_test/_util/module/pytmodtyping.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,43 +16,10 @@
#these two submodules unknowingly, which is simply horrid. Gah! I cry at night.

# ....................{ IMPORTS }....................
from beartype.typing import (
Any,
Iterable,
Union,
)
from beartype.typing import Any
from beartype._util.module.utilmodimport import import_module_attr_or_none
from beartype._util.module.lib.utiltyping import iter_typing_attrs

# ....................{ TESTERS }....................
def is_typing_attrs(typing_attr_basenames: Union[str, Iterable[str]]) -> bool:
'''
:data:`True` only if at least one quasi-standard typing module declares an
attribute with the passed basename.
Attributes
----------
typing_attr_basenames : Union[str, Iterable[str]]
Unqualified name of the attribute to be tested as existing in at least
one typing module.
Returns
----------
bool
:data:`True` only if at least one quasi-standard typing module declares
an attribute with this basename.
'''

# Return true only if at least one typing module defines an attribute with
# this name. Glory be to the obtuse generator comprehension expression!
return bool(tuple(iter_typing_attrs(
typing_attr_basenames=typing_attr_basenames,
is_warn=False,
)))

# ....................{ IMPORTERS }....................
#FIXME: Is this still required? Seriously. Shouldn't "typing_extensions"
#*ALWAYS* be sufficiently new under remote CI? *sigh*
def import_typing_attr_or_none_safe(typing_attr_basename: str) -> Any:
'''
Dynamically import and return the **typing attribute** (i.e., object
Expand All @@ -62,7 +29,7 @@ def import_typing_attr_or_none_safe(typing_attr_basename: str) -> Any:
otherwise (i.e., if this attribute is *not* importable from these modules).
Caveats
----------
-------
**This higher-level wrapper should typically be called in lieu of the
lower-level**
:func:`beartype._util.module.lib.utiltyping.import_typing_attr_or_none`
Expand All @@ -76,17 +43,17 @@ def import_typing_attr_or_none_safe(typing_attr_basename: str) -> Any:
Unqualified name of the attribute to be imported from a typing module.
Returns
----------
-------
object
Attribute with this name dynamically imported from a typing module.
Raises
----------
------
beartype.roar._roarexc._BeartypeUtilModuleException
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
Expand Down
38 changes: 28 additions & 10 deletions beartype_test/a00_unit/data/hint/pep/data_pep.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,13 @@
# Initialized by the _init() function below.
HINTS_PEP_META = []
'''
Tuple of **PEP-compliant type hint metadata** (i.e., :class:`HintPepMetadata`
Tuple of **PEP-compliant type hint metadata** (i.e.,
:class:`beartype_test.a00_unit.data.hint.util.data_hintmetacls.HintPepMetadata`
instances describing test-specific PEP-compliant type hints with metadata
leveraged by various testing scenarios).
Design
----------
------
This tuple was initially designed as a dictionary mapping from PEP-compliant
type hints to :class:`HintPepMetadata` instances describing those hints, until
:mod:`beartype` added support for PEPs enabling unhashable PEP-compliant type
Expand All @@ -141,6 +142,30 @@ def _init() -> None:
Initialize this submodule.
'''

#FIXME: Refactor this into a standard pytest fixture-based approach, please.
# Defer fixture-specific imports.
from beartype_test.a00_unit.data.hint.pep.proposal._data_pep589 import (
hints_pep_meta_pep589)

# Submodule globals to be redefined below.
global \
HINTS_PEP_HASHABLE, \
HINTS_PEP_IGNORABLE_DEEP, \
HINTS_PEP_IGNORABLE_SHALLOW, \
HINTS_PEP_META

# Tuple of all fixtures defining "HINTS_PEP_META" subiterables.
HINTS_PEP_META_FIXTURES = (
hints_pep_meta_pep589,
)

# For each fixture defining a "HINTS_PEP_META" subiterable, extend the main
# "HINTS_PEP_META" iterable by this subiterable.
for hints_pep_meta_fixture in HINTS_PEP_META_FIXTURES:
HINTS_PEP_META.extend(hints_pep_meta_fixture())

#FIXME: Excise almost everything below in favour of the standard pytest
#fixture-based approach above, please.
# Defer function-specific imports.
import sys
from beartype._util.utilobject import is_object_hashable
Expand All @@ -159,13 +184,6 @@ def _init() -> None:
_data_pep675,
)

# Submodule globals to be redefined below.
global \
HINTS_PEP_HASHABLE, \
HINTS_PEP_IGNORABLE_DEEP, \
HINTS_PEP_IGNORABLE_SHALLOW, \
HINTS_PEP_META

# Current submodule, obtained via the standard idiom. See also:
# https://stackoverflow.com/a/1676860/2809027
CURRENT_SUBMODULE = sys.modules[__name__]
Expand All @@ -177,7 +195,7 @@ def _init() -> None:
_data_pep544,
_data_pep585,
_data_pep586,
_data_pep589,
# _data_pep589,
_data_pep593,
_data_pep604,
_data_pep675,
Expand Down
23 changes: 15 additions & 8 deletions beartype_test/a00_unit/data/hint/pep/proposal/_data_pep586.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
'''

# ....................{ IMPORTS }....................
from beartype_test._util.module.pytmodtyping import is_typing_attrs

# ....................{ ADDERS }....................
def add_data(data_module: 'ModuleType') -> None:
Expand All @@ -22,13 +21,22 @@ def add_data(data_module: 'ModuleType') -> None:
Module to be added to.
'''

# If *NO* typing module declares a "Literal" factory, the active Python
# interpreter fails to support PEP 586. In this case, reduce to a noop.
if not is_typing_attrs('Literal'):
# print('Ignoring "Literal"...')
# ..................{ IMPORTS }..................
# Defer early-time fixture-specific imports.
from beartype._util.module.lib.utiltyping import (
is_typing_attr,
iter_typing_attrs,
)

# If *NO* currently importable "typing" module declares this type hint
# factory, the active Python interpreter fails to support this PEP. In this
# case, return the empty tuple.
if not is_typing_attr('Literal'):
return
# print('Testing "Literal"...')
# Else, this interpreter supports PEP 586.
# Else, this interpreter supports this PEP.

# ..................{ IMPORTS ~ moar }..................
# Defer all remaining fixture-specific imports.

# ..................{ IMPORTS }..................
# Defer data-dependent imports.
Expand All @@ -37,7 +45,6 @@ def add_data(data_module: 'ModuleType') -> None:
HintSignList,
HintSignLiteral,
)
from beartype._util.module.lib.utiltyping import iter_typing_attrs
from beartype_test.a00_unit.data.data_type import (
MasterlessDecreeVenomlessWhich)
from beartype_test.a00_unit.data.hint.util.data_hintmetacls import (
Expand Down

0 comments on commit 69e2fe4

Please sign in to comment.