Skip to content

Commit

Permalink
"numpy.typing.NDArray" x 3.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain adding portable support for
the third-party PEP-noncompliant `numpy.typing.NDArray` type hint newly
introduced with NumPy >= 1.21.0, en route to resolving issue #42 kindly
submitted by NumPy extraordinaire @Antyos. Specifically, this commit
refactors the pivotal
`beartype._util.hint.pep.utilpeptest.is_hint_pep()` tester to defer to
the even-more-pivotal
`beartype._util.hint.pep.utilpeptest.get_hint_pep_sign_or_none()`
getter, dramatically streamlining detection of PEP-compliant type hints.
(*Misbegotten hierophants misfire triumphantly!*)
  • Loading branch information
leycec committed Jul 22, 2021
1 parent 6f28002 commit 159ced5
Show file tree
Hide file tree
Showing 15 changed files with 195 additions and 131 deletions.
2 changes: 1 addition & 1 deletion beartype/_cave/_cavemap.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
BeartypeCaveNoneTypeOrKeyException,
BeartypeCaveNoneTypeOrMutabilityException,
)
from beartype._util.hint.nonpep.utilhintnonpeptest import (
from beartype._util.hint.nonpep.utilnonpeptest import (
die_unless_hint_nonpep)
from typing import Any, Tuple, Union

Expand Down
2 changes: 1 addition & 1 deletion beartype/_decor/_error/_errortype.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from beartype.roar._roarexc import _BeartypeCallHintPepRaiseException
from beartype._decor._error._errorsleuth import CauseSleuth
from beartype._util.data.hint.pep.sign.datapepsigns import HintSignForwardRef
from beartype._util.hint.nonpep.utilhintnonpeptest import (
from beartype._util.hint.nonpep.utilnonpeptest import (
die_unless_hint_nonpep_tuple)
from beartype._util.hint.pep.utilpepget import (
get_hint_pep_type_stdlib_or_none)
Expand Down
11 changes: 10 additions & 1 deletion beartype/_util/data/hint/pep/datapeprepr.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,18 @@
HINT_TYPE_NAME_TO_SIGN: Dict[str, HintSign] = {
# ..................{ PEP 484 }..................
# PEP 484-compliant forward reference type hints may be annotated either:
# * Implicitly as strings, which this key-value pair here detects.
# * Explicitly as "typing.ForwardRef" instances, which automated inspection
# below in the _init() function detects.
# * Implicitly as strings, which this key-value pair here detects. Note
# this unconditionally matches *ALL* strings, including both:
# * Invalid Python identifiers (e.g., "0d@yw@r3z").
# * Absolute forward references (i.e., fully-qualified classnames)
# technically non-compliant with PEP 484 but seemingly compliant with
# PEP 585.
# Since the distinction between PEP-compliant and -noncompliant forward
# references is murky at best and since unconditionally matching *ALL*
# string as PEP-compliant substantially simplifies logic throughout the
# codebase, we (currently) opt to do so.
'builtins.str': HintSignForwardRef,
}
'''
Expand Down
10 changes: 5 additions & 5 deletions beartype/_util/func/utilfuncscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,7 @@ def add_func_scope_types(
scoped attribute externally accessed elsewhere, whose key is a
machine-readable name internally generated by this function to uniquely
refer to the passed tuple of types and whose value is that tuple) to the
passed scope *and* return that name.
passed scope *and* return that machine-readable name.
This function additionally caches this tuple with the beartypistry
singleton to reduce space consumption for tuples duplicated across the
Expand All @@ -701,7 +701,7 @@ def add_func_scope_types(
Unlike types, tuples are commonly dynamically constructed on-the-fly by
various tuple factories (e.g., :attr:`beartype.cave.NoneTypeOr`,
:attr:`typing.Optional`) and hence have no reliable fully-qualified names.
Instead, this closure caches this tuple into the beartypistry under a
Instead, this function caches this tuple into the beartypistry under a
string synthesized as the unique concatenation of:
* The magic substring :data:`TYPISTRY_HINT_NAME_TUPLE_PREFIX`. Since
Expand Down Expand Up @@ -733,7 +733,7 @@ def add_func_scope_types(
Identifying tuples by their hashes enables the beartypistry singleton to
transparently cache duplicate class tuples with distinct object IDs as the
same underlying object, reducing space consumption. While hashing tuples
does impact time performance, the tangible gains are worth the cost.
does impact time performance, the gain in space is worth the cost.
Parameters
----------
Expand Down Expand Up @@ -794,7 +794,7 @@ def add_func_scope_types(
TYPISTRY_HINT_NAME_TUPLE_PREFIX,
bear_typistry,
)
from beartype._util.hint.nonpep.utilhintnonpeptest import (
from beartype._util.hint.nonpep.utilnonpeptest import (
die_unless_hint_nonpep_type)

# If this object is neither a set nor tuple, raise an exception.
Expand All @@ -808,7 +808,7 @@ def add_func_scope_types(
raise BeartypeDecorHintNonPepException(f'{types_label} empty.')
# Else, this collection is non-empty.

# If any item of this collection is *NOT* a standard class, raise an
# If any item in this collection is *NOT* a standard class, raise an
# exception.
for cls in types:
die_unless_hint_nonpep_type(hint=cls, hint_label=types_label)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# See "LICENSE" for further details.

'''
**Beartype PEP-noncompliant type hint utilities.**
Project-wide **PEP-noncompliant type hint** utilities.
This private submodule is *not* intended for importation by downstream callers.
'''
Expand All @@ -29,6 +29,82 @@
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ VALIDATORS }....................
#FIXME: Unit test us up, please.
def die_if_hint_nonpep(
# Mandatory parameters.
hint: object,

# Optional parameters.
hint_label: str = 'Type hint',
is_str_valid: bool = True,
exception_cls: Type[Exception] = BeartypeDecorHintNonPepException,
) -> None:
'''
Raise an exception if the passed object is a **PEP-noncompliant type hint**
(i.e., :mod:`beartype`-specific annotation *not* compliant with
annotation-centric PEPs).
This validator is effectively (but technically *not*) memoized. See the
:func:`beartype._util.hint.utilhinttest.die_unless_hint` validator.
Parameters
----------
hint : object
Object to be validated.
hint_label : Optional[str]
Human-readable label prefixing this object's representation in the
exception message raised by this function. Defaults to ``Type hint``.
is_str_valid : Optional[bool]
``True`` only if this function permits this object to either be a
string or contain strings. Defaults to ``True``. If this boolean is:
* ``True``, this object is valid only if this object is either a class
or tuple of classes and/or classnames.
* ``False``, this object is valid only if this object is either a class
or tuple of classes.
exception_cls : Optional[type]
Type of the exception to be raised by this function. Defaults to
:class:`BeartypeDecorHintNonPepException`.
Raises
----------
exception_cls
If this object is either:
* An **isinstanceable type** (i.e., standard class passable as the
second parameter to the :func:`isinstance` builtin and thus typically
*not* compliant with annotation-centric PEPs).
* A **non-empty tuple** (i.e., semantic union of types) containing one
or more:
* Non-:mod:`typing` types.
* If ``is_str_valid``, **strings** (i.e., forward references
specified as either fully-qualified or unqualified classnames).
'''

# If this object is a PEP-noncompliant type hint, raise an exception.
#
# Note that this memoized call is intentionally passed positional rather
# than keyword parameters to maximize efficiency.
if is_hint_nonpep(hint, is_str_valid):
assert isinstance(hint_label, str), f'{repr(hint_label)} not string.'
assert isinstance(exception_cls, type), (
f'{repr(exception_cls)} not type.')

raise exception_cls(
f'{hint_label} {repr(hint)} is PEP-noncompliant (e.g., ' +
(
(
'isinstanceable class, forward reference, or tuple of '
'isinstanceable classes and/or forward references).'
)
if is_str_valid else
'isinstanceable class or tuple of isinstanceable classes).'
)
)
# Else, this object is *NOT* a PEP-noncompliant type hint.


#FIXME: Unit test this function with respect to non-isinstanceable classes.
def die_unless_hint_nonpep(
# Mandatory parameters.
Expand All @@ -55,8 +131,8 @@ def die_unless_hint_nonpep(
Human-readable label prefixing this object's representation in the
exception message raised by this function. Defaults to ``Type hint``.
is_str_valid : Optional[bool]
``True`` only if this function permits this object to be a string.
Defaults to ``True``. If this boolean is:
``True`` only if this function permits this object to either be a
string or contain strings. Defaults to ``True``. If this boolean is:
* ``True``, this object is valid only if this object is either a class
or tuple of classes and/or classnames.
Expand All @@ -68,18 +144,12 @@ def die_unless_hint_nonpep(
Raises
----------
TypeError
If this object is **unhashable** (i.e., *not* hashable by the builtin
:func:`hash` function and thus unusable in hash-based containers like
dictionaries and sets). All supported type hints are hashable.
exception_cls
If this object is neither:
* A non-:mod:`typing` type (i.e., class *not* defined by the
:mod:`typing` module, whose public classes are used to instantiate
PEP-compliant type hints or objects satisfying such hints that
typically violate standard class semantics and thus require
PEP-specific handling).
* An **isinstanceable type** (i.e., standard class passable as the
second parameter to the :func:`isinstance` builtin and thus typically
*not* compliant with annotation-centric PEPs).
* A **non-empty tuple** (i.e., semantic union of types) containing one
or more:
Expand Down Expand Up @@ -135,19 +205,19 @@ def die_unless_hint_nonpep(
raise exception_cls(
f'{hint_label} {repr(hint)} '
f'neither PEP-compliant nor -noncompliant '
f'(e.g., standard class, forward reference, or '
f'tuple of standard classes and forward references).'
f'(e.g., isinstanceable class, forward reference, or '
f'tuple of isinstanceable classes and forward references).'
)
# Else, forward references are unsupported. In this case, raise an
# exception noting that.
else:
raise exception_cls(
f'{hint_label} {repr(hint)} '
f'neither PEP-compliant nor -noncompliant '
f'(e.g., standard class or tuple of standard classes).'
f'(e.g., isinstanceable class or tuple of isinstanceable classes).'
)


# ....................{ VALIDATORS ~ kind }....................
#FIXME: Unit test us up.
def die_unless_hint_nonpep_type(
# Mandatory parameters.
Expand Down Expand Up @@ -208,13 +278,14 @@ def die_unless_hint_nonpep_type(
#internally raise a stop iteration exception, whereas EAFP only raises an
#exception if this tuple is invalid, in which case efficiency is no longer a
#concern. So, what do we do instead? Simple. We internally refactor:
#
#* If "is_str_valid" is True, we continue to perform the existing
# implementation of both functions. *shrug*
#* Else, we:
# * Perform a new optimized EAFP-style isinstance() check resembling that
# performed by die_unless_type_isinstanceable().
# * Likewise for _is_hint_nonpep_tuple() vis-a-vis is_type_isinstanceable().
#Fortunately, tuple unions are now sufficiently rare in the wild (i.e., in
#real-world use cases) that this mild inefficiency probably no longer matters.
#FIXME: Unit test this function with respect to tuples containing
#non-isinstanceable classes.
def die_unless_hint_nonpep_tuple(
Expand Down Expand Up @@ -242,24 +313,18 @@ def die_unless_hint_nonpep_tuple(
Human-readable label prefixing this object's representation in the
exception message raised by this function. Defaults to ``Type hint``.
is_str_valid : bool, optional
``True`` only if this function permits this object to be a string. If:
* ``True``, this object is valid only if this object is either a class
or tuple of classes and/or classnames.
* ``False``, this object is valid only if this object is either a class
or tuple of classes.
``True`` only if this function permits this tuple to contain strings.
Defaults to ``False``. If:
Defaults to ``False``.
* ``True``, this tuple is valid only when containing classes and/or
classnames.
* ``False``, this tuple is valid only when containing classes.
exception_cls : type, optional
Type of the exception to be raised by this function. Defaults to
:class:`BeartypeDecorHintNonPepException`.
Raises
----------
TypeError
If this object is **unhashable** (i.e., *not* hashable by the builtin
:func:`hash` function and thus unusable in hash-based containers like
dictionaries and sets). All supported type hints are hashable.
exception_cls
If this object is neither:
Expand Down Expand Up @@ -428,13 +493,12 @@ def _is_hint_nonpep_tuple(
hint : object
Object to be inspected.
is_str_valid : Optional[bool]
``True`` only if this function permits this object to be a string.
``True`` only if this function permits this tuple to contain strings.
Defaults to ``True``. If this boolean is:
* ``True``, this object is valid only if this object is a tuple of
classes and/or classnames.
* ``False``, this object is valid only if this object is a tuple of
classes.
* ``True``, this tuple is valid only when containing classes and/or
classnames.
* ``False``, this object is valid only when containing classes.
Returns
----------
Expand Down
27 changes: 14 additions & 13 deletions beartype/_util/hint/pep/utilpepget.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,11 +291,12 @@ def get_hint_pep_sign(hint: Any) -> HintSign:
Raises
----------
BeartypeDecorHintPepException
If this hint is *not* PEP-compliant.
BeartypeDecorHintPepSignException
If this object is a PEP-compliant type hint *not* uniquely identifiable
by a sign.
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).
See Also
----------
Expand All @@ -307,12 +308,15 @@ def get_hint_pep_sign(hint: Any) -> HintSign:
# If this hint is unrecognized...
if hint_sign is None:
# Avoid circular import dependencies.
from beartype._util.hint.pep.utilpeptest import die_unless_hint_pep
from beartype._util.hint.nonpep.utilnonpeptest import (
die_if_hint_nonpep)

# If this hint is *NOT* PEP-compliant, raise an exception.
die_unless_hint_pep(hint)
# Else, this hint is PEP-compliant. Since this hint is unrecognized,
# this hint *MUST* be currently unsupported by the @beartype decorator.
# If this hint is PEP-noncompliant, raise an exception.
die_if_hint_nonpep(
hint=hint, exception_cls=BeartypeDecorHintPepSignException)
# Else, this hint is *NOT* PEP-noncompliant. Since this hint was
# unrecognized, this hint *MUST* necessarily be a PEP-compliant type
# hint currently unsupported by the @beartype decorator.

# Raise an exception indicating this.
#
Expand All @@ -325,7 +329,7 @@ def get_hint_pep_sign(hint: Any) -> HintSign:
f'a feature request for this hint to our '
f'friendly issue tracker at:\n\t{URL_ISSUES}'
)
# Else, this hint is unrecognized.
# Else, this hint is recognized.

# Return the sign uniquely identifying this hint.
return hint_sign
Expand All @@ -334,9 +338,6 @@ def get_hint_pep_sign(hint: Any) -> HintSign:
#FIXME: Test that our "testing_extensions.Annotated" support actually works.
#FIXME: Revise us up the docstring, most of which is now obsolete.
#FIXME: Refactor as follows:
#* Refactor the following functions to mostly defer to that new
# get_hint_pep_sign_or_none() function:
# * The is_hint_pep() function.
#* Remove all now-unused "beartype._util.hint.pep.*" testers. Thanks to this
# dramatically simpler approach, we no longer require the excessive glut of
# PEP-specific testers we previously required.
Expand Down
Loading

0 comments on commit 159ced5

Please sign in to comment.