Skip to content

Commit

Permalink
Callable[...] introspection x 1.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain defining a new internal
private API for introspecting ``Callable[...]`` type hints at runtime,
required as a mandatory dependency of pull request #136 kindly submitted
by Harvard microscopist and all-around typing math guru @tlambert03
(Talley Lambert). Specifically, this commit defines a new private
`beartype._util.hint.pep.proposal.pep484585.utilpep484585callable._die_unless_hint_pep484585_callable()`
validator validating the passed object to be such a type hint as well
as preliminary tests exercising this functionality. (*Insecure sinecure!*)
  • Loading branch information
leycec committed Jun 9, 2022
1 parent 8ae0164 commit 4fb8b1c
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 49 deletions.
Expand Up @@ -19,6 +19,7 @@
Union,
)
from beartype._data.datatyping import TypeException
from beartype._data.hint.pep.sign.datapepsigns import HintSignCallable
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_10

# See the "beartype.cave" submodule for further commentary.
Expand Down Expand Up @@ -59,10 +60,67 @@
``typing.Callable[...]`` or ``collections.abc.Callable[...]`` type hint).
'''

# ....................{ VALIDATORS }....................
def _die_unless_hint_pep484585_callable(
# Mandatory parameters.
hint: object,

# Optional parameters.
exception_cls: TypeException = BeartypeDecorHintPep484585Exception,
exception_prefix: str = '',
) -> None:
'''
Raise an exception unless the passed object is either a :pep:`484`- or
:pep:`585`-compliant **callable type hint** (i.e., ``typing.Callable[...]``
or ``collections.abc.Callable[...]`` type hint).
Parameters
----------
hint : object
Object to be validated.
exception_cls : TypeException, optional
Type of exception to be raised. Defaults to
:exc:`BeartypeDecorHintPep484585Exception`.
exception_prefix : str, optional
Human-readable label prefixing the representation of this object in the
exception message. Defaults to the empty string.
Raises
----------
BeartypeDecorHintPepSignException
If this hint is either:
* PEP-compliant but *not* uniquely identifiable by a sign.
* PEP-noncompliant.
* *Not* a hint (i.e., neither PEP-compliant nor -noncompliant).
:exc:`exception_cls`
If this hint is *not* a callable type hint.
'''

# Avoid circular import dependencies.
from beartype._util.hint.pep.utilpepget import get_hint_pep_sign

# Sign uniquely identifying this hint if any *OR* raise an exception.
hint_sign = get_hint_pep_sign(hint)

# If this object is *NOT* a callable type hint, raise an exception.
if hint_sign is not HintSignCallable:
assert issubclass(exception_cls, Exception), (
f'{repr(exception_cls)} not exception class.')
assert isinstance(exception_prefix, str), (
f'{repr(exception_prefix)} not string.')

raise exception_cls(
f'{exception_prefix}type hint {repr(hint)} not '
f'PEP 484 or 585 callable type hint '
f'(i.e., "typing.Callable[...]" or '
f'"collections.abc.Callable[...]").'
)
# Else, this object is a callable type hint, raise an exception.

# ....................{ GETTERS }....................
#FIXME: Implement a new _die_unless_hint_pep484585_callable() validator, please.
#FIXME: Implement us up, please.
#FIXME: Unit test us up, please.
#FIXME: Unit test us up, please. Note that we should exercise *ALL* edge cases
#by defining sample type hints doing so in our test suite.
def get_hint_pep484585_callable_args(
# Mandatory parameters.
hint: object,
Expand Down Expand Up @@ -96,7 +154,7 @@ def get_hint_pep484585_callable_args(
----------
hint : object
Callable type hint to be inspected.
exception_cls : TypeException
exception_cls : TypeException, optional
Type of exception to be raised. Defaults to
:exc:`BeartypeDecorHintPep484585Exception`.
exception_prefix : str, optional
Expand All @@ -114,4 +172,9 @@ def get_hint_pep484585_callable_args(
If this hint is *not* a callable type hint.
'''

pass
# If this hint is *NOT* a callable type hint, raise an exception.
_die_unless_hint_pep484585_callable(hint)
# Else, this hint is a callable type hint.

#FIXME: Implement us up, please.
return ()
25 changes: 16 additions & 9 deletions beartype/_util/hint/pep/proposal/pep484585/utilpep484585ref.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2022 Beartype authors.
# See "LICENSE" for further details.

Expand All @@ -11,8 +11,12 @@
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS }....................
# ....................{ IMPORTS }....................
from beartype.roar import BeartypeDecorHintForwardRefException
from beartype.typing import (
Any,
Union,
)
from beartype._util.cls.utilclstest import die_unless_type
from beartype._util.hint.pep.proposal.pep484.utilpep484ref import (
HINT_PEP484_FORWARDREF_TYPE,
Expand All @@ -22,12 +26,11 @@
from beartype._util.mod.utilmodimport import import_module_attr
from beartype._util.mod.utilmodget import get_object_module_name
from beartype._data.datatyping import TypeException
from typing import Any, Union

# See the "beartype.cave" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ HINTS }....................
# ....................{ HINTS }....................
HINT_PEP484585_FORWARDREF_TYPES = (str, HINT_PEP484_FORWARDREF_TYPE)
'''
Tuple union of all :pep:`484`- or :pep:`585`-compliant **forward reference
Expand Down Expand Up @@ -61,7 +64,7 @@
Further details.
'''

# ....................{ VALIDATORS ~ kind : forwardref }....................
# ....................{ VALIDATORS }....................
def die_unless_hint_pep484585_forwardref(
# Mandatory parameters.
hint: object,
Expand Down Expand Up @@ -94,18 +97,22 @@ def die_unless_hint_pep484585_forwardref(
Raises
----------
:exc:`BeartypeDecorHintForwardRefException`
BeartypeDecorHintForwardRefException
If this object is *not* a forward reference type hint.
'''

# If this is *NOT* a forward reference type hint, raise an exception.
# If this object is *NOT* a forward reference type hint, raise an exception.
if not isinstance(hint, HINT_PEP484585_FORWARDREF_TYPES):
assert isinstance(exception_prefix, str), (
f'{repr(exception_prefix)} not string.')

raise BeartypeDecorHintForwardRefException(
f'{exception_prefix}type hint {repr(hint)} not forward reference '
f'(i.e., neither string nor "typing.ForwardRef" instance).'
)
# Else, this object is a forward reference type hint.

# ....................{ GETTERS ~ kind : forwardref }....................
# ....................{ GETTERS ~ kind : forwardref }....................
#FIXME: Unit test against nested classes.
#FIXME: Validate that this forward reference string is *NOT* the empty string.
#FIXME: Validate that this forward reference string is a syntactically valid
Expand Down Expand Up @@ -228,7 +235,7 @@ def get_hint_pep484585_forwardref_classname_relative_to_object(
f'{get_object_module_name(obj)}.{forwardref_classname}'
)

# ....................{ IMPORTERS }....................
# ....................{ IMPORTERS }....................
#FIXME: Unit test us up, please.
def import_pep484585_forwardref_type_relative_to_object(
# Mandatory parameters.
Expand Down
19 changes: 10 additions & 9 deletions beartype/_util/hint/pep/proposal/utilpep585.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2022 Beartype authors.
# See "LICENSE" for further details.

Expand All @@ -9,7 +9,7 @@
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS }....................
# ....................{ IMPORTS }....................
from beartype.roar import BeartypeDecorHintPep585Exception
from beartype._cave._cavefast import HintGenericSubscriptedType
from beartype._util.cache.utilcachecall import callable_cached
Expand All @@ -21,7 +21,7 @@
# See the "beartype.cave" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ HINTS }....................
# ....................{ HINTS }....................
HINT_PEP585_TUPLE_EMPTY = (
tuple[()] if IS_PYTHON_AT_LEAST_3_9 else Iota()) # type: ignore[misc]
'''
Expand All @@ -31,7 +31,7 @@
objects against this object via equality tests.
'''

# ....................{ VALIDATORS }....................
# ....................{ VALIDATORS }....................
def die_unless_hint_pep585_generic(hint: object) -> None:
'''
Raise an exception unless the passed object is a :pep:`585`-compliant
Expand All @@ -46,15 +46,16 @@ def die_unless_hint_pep585_generic(hint: object) -> None:
Raises
----------
BeartypeDecorHintPep585Exception
If this hint is *not* a :pep:`585`-compliant generic.
If this object is *not* a :pep:`585`-compliant generic.
'''

# If this hint is *NOT* a PEP 585-compliant generic, raise an exception
# If this object is *NOT* a PEP 585-compliant generic, raise an exception.
if not is_hint_pep585_generic(hint):
raise BeartypeDecorHintPep585Exception(
f'Type hint {repr(hint)} not PEP 585 generic.')
# Else, this object is a PEP 585-compliant generic.

# ....................{ TESTERS }....................
# ....................{ TESTERS }....................
# If the active Python interpreter targets at least Python >= 3.9 and thus
# supports PEP 585, correctly declare this function.
if IS_PYTHON_AT_LEAST_3_9:
Expand Down Expand Up @@ -139,7 +140,7 @@ def is_hint_pep585_builtin(hint: object) -> bool:
def is_hint_pep585_generic(hint: object) -> bool:
return False

# ....................{ TESTERS ~ doc }....................
# ....................{ TESTERS ~ doc }....................
# Docstring for this function regardless of implementation details.
is_hint_pep585_builtin.__doc__ = '''
``True`` only if the passed object is a C-based :pep:`585`-compliant
Expand Down Expand Up @@ -198,7 +199,7 @@ def is_hint_pep585_generic(hint: object) -> bool:
``True`` only if this object is a :pep:`585`-compliant generic.
'''

# ....................{ GETTERS }....................
# ....................{ GETTERS }....................
def get_hint_pep585_generic_bases_unerased(hint: Any) -> tuple:
'''
Tuple of all unerased :pep:`585`-compliant **pseudo-superclasses** (i.e.,
Expand Down
16 changes: 8 additions & 8 deletions beartype/_util/mod/utilmodtest.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2022 Beartype authors.
# See "LICENSE" for further details.

Expand All @@ -10,16 +10,16 @@
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS }....................
# ....................{ IMPORTS }....................
from beartype.meta import _convert_version_str_to_tuple
from beartype.roar._roarexc import _BeartypeUtilModuleException
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_8
from beartype._data.datatyping import TypeException
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_8

# See the "beartype.cave" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ VALIDATORS }....................
# ....................{ VALIDATORS }....................
def die_unless_module_attr_name(
# Mandatory parameters.
module_attr_name: str,
Expand Down Expand Up @@ -49,7 +49,7 @@ def die_unless_module_attr_name(
Raises
----------
exception_cls
:exc:`exception_cls`
If either:
* This name is *not* a string.
Expand Down Expand Up @@ -103,7 +103,7 @@ def die_unless_module_attr_name(
# Else, this string is syntactically valid as a fully-qualified module
# attribute name.

# ....................{ TESTERS }....................
# ....................{ TESTERS }....................
def is_module(module_name: str) -> bool:
'''
``True`` only if the module or C extension with the passed fully-qualified
Expand All @@ -126,7 +126,7 @@ def is_module(module_name: str) -> bool:
Warns
----------
:class:`BeartypeModuleUnimportableWarning`
BeartypeModuleUnimportableWarning
If a module with this name exists *but* that module is unimportable
due to raising module-scoped exceptions at importation time.
'''
Expand Down Expand Up @@ -296,7 +296,7 @@ def is_module_version_at_least(module_name: str, version_minimum: str) -> bool:
# Return true only if this requirement is satisfied.
return module_distribution is not None

# ....................{ TESTERS ~ package }....................
# ....................{ TESTERS ~ package }....................
#FIXME: Unit test us up, please.
def is_package(package_name: str) -> bool:
'''
Expand Down
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2022 Beartype authors.
# See "LICENSE" for further details.

'''
Project-wide :pep:`484` and :pep:`585` **callable type hint utility unit
tests.**
This submodule unit tests the public API of the private
:mod:`beartype._util.hint.pep.proposal.pep484585.utilpep484585callable` submodule.
'''

# ....................{ IMPORTS }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# WARNING: To raise human-readable test errors, avoid importing from
# package-specific submodules at module scope.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

# ....................{ TESTS ~ kind : callable }....................
def test_get_hint_pep484585_callable_args() -> None:
'''
Test the
:func:`beartype._util.hint.pep.proposal.pep484585.utilpep484585callable.get_hint_pep484585_callable_args`
getter.
'''

# Defer heavyweight imports.
from beartype.roar import BeartypeDecorHintPep484585Exception
from beartype._data.hint.pep.sign.datapepsigns import HintSignCallable
from beartype._util.hint.pep.proposal.pep484585.utilpep484585callable import (
get_hint_pep484585_callable_args)
from beartype_test.a00_unit.data.hint.data_hint import NOT_HINTS_PEP
from beartype_test.a00_unit.data.hint.pep.data_pep import HINTS_PEP_META
from pytest import raises

# Assert this getter...
for hint_pep_meta in HINTS_PEP_META:
# Returns zero or more arguments for PEP-compliant callable type hints.
if hint_pep_meta.pep_sign is HintSignCallable:
hint_callable_args = get_hint_pep484585_callable_args(
hint_pep_meta.hint)
assert isinstance(hint_callable_args, tuple)
# Raises an exception for concrete PEP-compliant type hints *NOT*
# defined by the "typing" module.
else:
with raises(BeartypeDecorHintPep484585Exception):
get_hint_pep484585_callable_args(hint_pep_meta.hint)

#FIXME: *UGH.* This unhelpfully raises a
#"BeartypeDecorHintPepSignException". Let's generalize our low-level
#get_hint_pep_sign() getter to accept an optional "exception_cls", please.
# # Assert this getter raises the expected exception for non-PEP-compliant
# # type hints.
# for not_hint_pep in NOT_HINTS_PEP:
# with raises(BeartypeDecorHintPep484585Exception):
# get_hint_pep484585_callable_args(not_hint_pep)

0 comments on commit 4fb8b1c

Please sign in to comment.