Skip to content

Commit

Permalink
Private hint data sanitization x 2.
Browse files Browse the repository at this point in the history
This commit is the last in a commit chain sanitizing the private
"beartype._util.hint.data" subpackage for improved robustness. The prior
design of this subpackage was deeply flawed, promoting fragile and
subtle issues throughout the codebase that were dependent on importation
order and thus non-trivial to reproduce. Unrelatedly, this commit also
resolves a significant issue in a `mypy`-based functional test
validating `beartype` to be PEP 561-compliant. (*Buzzsaw buzzkill!*)
  • Loading branch information
leycec committed Feb 27, 2021
1 parent 718baaa commit e25cde7
Show file tree
Hide file tree
Showing 21 changed files with 290 additions and 180 deletions.
26 changes: 24 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,6 @@ dependency <Sphinx_>`__. Beartype supports `all actively developed Python
versions <Python status_>`__, `all Python package managers <Install_>`__, and
`multiple platform-specific package managers <Install_>`__.
*Quality assurance has now been assured.*
.. # ------------------( TABLE OF CONTENTS )------------------
.. # Blank line. By default, Docutils appears to only separate the subsequent
.. # table of contents heading from the prior paragraph by less than a single
Expand All @@ -94,6 +92,30 @@ versions <Python status_>`__, `all Python package managers <Install_>`__, and
.. # ------------------( DESCRIPTION )------------------
tl;dr
=====
#. `Install beartype <Install_>`__:
.. code-block:: shell-session
pip3 install beartype
#. `Decorate functions and methods annotated by PEP-compliant type hints
with the @beartype.beartype decorator <Usage_>`__:
.. code-block:: python
from beartype import beartype
from collections.abc import Iterable
from typing import Optional
@beartype
def print_messages(messages: Optional[Iterable[str]] = ('Hello, world.',)):
print('\n'.join(messages))
*Quality assurance has now been assured.*
News
====
Expand Down
2 changes: 1 addition & 1 deletion beartype/_decor/_cache/cachehint.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
#
#Specifically, rather than accept "typing" nonsense verbatim, we could instead:
#* Detect PEP 544-compatible protocol type hints *NOT* decorated by
# @typing.runtime_checkable. We have an existing tester somewhere that now
# @typing.runtime_checkable. The existing is_type_isinstanceable() tester now
# detects whether arbitrary classes are isinstanceable, so just call that.
#* Emit a non-fatal warning advising the end user to resolve this on their end.
#* Meanwhile, beartype can simply:
Expand Down
72 changes: 30 additions & 42 deletions beartype/_decor/_cache/cachetype.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
is_classname_builtin,
)
from beartype._util.utilobject import (
get_object_classname,
get_object_class_basename,
get_object_type_name,
get_object_type_basename,
)
from typing import Tuple

Expand Down Expand Up @@ -79,9 +79,10 @@
'''

# ....................{ REGISTRARS ~ forwardref }....................
#FIXME: Refactor this function to optionally accept a "hint_label" parameter,
#much like the registery_typistry_type() function.
#FIXME: Unit test us up.

# Note this function intentionally does *NOT* accept an optional "hint_labal"
# parameter as doing so would conflict with memoization.
@callable_cached
def register_typistry_forwardref(hint_classname: str) -> str:
'''
Expand Down Expand Up @@ -119,7 +120,7 @@ def register_typistry_forwardref(hint_classname: str) -> str:
'''

# If this object is *NOT* the syntactically valid fully-qualified name of a
# module attribute that may or may not actually exist, raise an exception.
# module attribute which may *NOT* actually exist, raise an exception.
die_unless_module_attr_name(
module_attr_name=hint_classname,
exception_label='Forward reference',
Expand Down Expand Up @@ -164,17 +165,12 @@ def register_typistry_forwardref(hint_classname: str) -> str:
#Given that, we should probably just raise an exception here if this hint is
#subscripted and require our caller to explicitly strip this subscripted
#generic to its original unsubscripted class (i.e., "hint.__origin").
#FIXME: Refactor all calls to this function to pass the "hint_label" parameter.
#FIXME: Exercise this with a unit test, please!

# Note this function intentionally does *NOT* accept an optional "hint_labal"
# parameter as doing so would conflict with memoization.
@callable_cached
def register_typistry_type(
# Mandatory parameters.
hint: type,

# Optional parameters.
hint_label: str = 'Annotated',
) -> str:
def register_typistry_type(hint: type) -> str:
'''
Register the passed **unsubscripted PEP-noncompliant type** (i.e., class
neither defined by the :mod:`typing` module *nor* subclassing such a class
Expand All @@ -187,8 +183,8 @@ def register_typistry_type(
codebase, but is otherwise roughly equivalent to:
>>> from beartype._decor._cache.cachetype import bear_typistry
>>> from beartype._util.utilobject import get_object_classname
>>> bear_typistry[get_object_classname(hint)] = hint
>>> from beartype._util.utilobject import get_object_type_name
>>> bear_typistry[get_object_type_name(hint)] = hint
This function is memoized for both efficiency *and* safety, preventing
accidental re-registration of previously registered types.
Expand All @@ -197,9 +193,6 @@ def register_typistry_type(
----------
hint : type
Unsubscripted PEP-noncompliant type to be registered.
hint_label : str, optional
Human-readable label prefixing this type's representation in the
exception message raised by this function. Defaults to ``"Annotated"``.
Returns
----------
Expand All @@ -211,17 +204,16 @@ def register_typistry_type(
Raises
----------
BeartypeDecorHintTypeException
If this object is *not* an *isinstanceable class** (i.e., class whose
metaclass does *not* define an ``__instancecheck__()`` dunder method
that raises an exception).
_BeartypeDecorBeartypistryException
If this object is either:
* *Not* a class.
* **PEP-compliant** (i.e., either a class defined by the :mod:`typing`
module *or* subclass of such a class and thus a PEP-compliant type
hint, which all violate standard type semantics and thus require
PEP-specific handling).
* *Not* an *isinstanceable class** (i.e., class whose metaclass does
*not* define an ``__instancecheck__()`` dunder method that raises an
exception).
.. _PEP 484:
https://www.python.org/dev/peps/pep-0484
Expand All @@ -232,7 +224,7 @@ def register_typistry_type(
'''

# If this object is *NOT* an isinstanceable class, raise an exception.
die_unless_hint_type_isinstanceable(hint=hint, hint_label=hint_label)
die_unless_hint_type_isinstanceable(hint)
# Else, this object is an isinstanceable class.
#
# Note that we defer all further validation of this type to the
Expand All @@ -243,12 +235,12 @@ def register_typistry_type(
# requiring *no* explicit importation), this type requires no registration.
# In this case, return the unqualified basename of this type as is.
if is_type_builtin(hint):
return get_object_class_basename(hint)
return get_object_type_basename(hint)
# Else, this type is *NOT* a builtin and thus requires registration.
# assert hint_basename != 'NoneType'

# Fully-qualified name of this type.
hint_classname = get_object_classname(hint)
hint_classname = get_object_type_name(hint)

# If this type has yet to be registered with the beartypistry singleton, do
# so.
Expand All @@ -267,16 +259,14 @@ def register_typistry_type(
)

# ....................{ REGISTRARS ~ tuple }....................
#FIXME: Refactor all calls to this function to pass the "hint_label" parameter.
#FIXME: Exercise this function with respect to tuples containing one or more
#non-isinstanceable types, please.
# Note this function intentionally does *NOT* accept an optional "hint_labal"
# parameter as doing so would conflict with memoization.
@callable_cached
def register_typistry_tuple(
# Mandatory parameters.
hint: Tuple[type, ...],

# Optional parameters.
hint_label: str = 'Annotated',
is_types_unique: bool = False,
) -> str:
'''
Expand Down Expand Up @@ -334,9 +324,6 @@ def register_typistry_tuple(
----------
hint : Tuple[type]
Tuple of all PEP-noncompliant types to be registered.
hint_label : str, optional
Human-readable label prefixing this type's representation in the
exception message raised by this function. Defaults to ``"Annotated"``.
is_types_unique : bool
``True`` only if the caller guarantees this tuple to contain *no*
duplicates. If ``False``, this function assumes this tuple to contain
Expand Down Expand Up @@ -364,26 +351,27 @@ def register_typistry_tuple(
Raises
----------
_BeartypeDecorBeartypistryException
BeartypeDecorHintNonPepException
If this tuple is either:
* *Not* a tuple.
* Is a tuple containing one or more items that are either:
* Is a tuple containing any item that is either:
* *Not* types.
* **PEP-compliant types** (i.e., either classes defined by the
:mod:`typing` module *or* subclasses of such classes and thus
PEP-compliant type hints, which all violate standard type semantics
* *Not* a type.
* A **PEP-compliant type** (i.e., class either defined by the
:mod:`typing` module *or* subclass of such a class and thus
PEP-compliant type hint, which all violate standard type semantics
and thus require PEP-specific handling).
* *Not* an *isinstanceable class** (i.e., class whose metaclass does
*not* define an ``__instancecheck__()`` dunder method that raises
an exception).
'''
assert isinstance(is_types_unique, bool), (
f'{repr(is_types_unique)} not bool.')

# If this object is *NOT* an isinstanceable tuple, raise an exception.
die_unless_hint_nonpep_tuple(
hint=hint,
hint_label=hint_label,

#FIXME: Actually, we eventually want to permit this to enable
#trivial resolution of forward references. For now, this is fine.
is_str_valid=False,
Expand Down Expand Up @@ -572,7 +560,7 @@ def __setitem__(self, hint_name: str, hint: object) -> None:
# distinguishing between either here.
elif isinstance(hint, type):
# Fully-qualified classname of this type as declared by this type.
hint_clsname = get_object_classname(hint)
hint_clsname = get_object_type_name(hint)

# If...
if (
Expand Down Expand Up @@ -684,7 +672,7 @@ def __missing__(self, hint_classname: str) -> type:
# then implicitly maps the passed missing key to this class by
# effectively assigning this name to this class: e.g.,
# self[hint_classname] = hint_class
return hint_class
return hint_class # type: ignore[return-value]

# ....................{ SINGLETONS }....................
bear_typistry = Beartypistry()
Expand Down
44 changes: 29 additions & 15 deletions beartype/_decor/_code/_pep/_pephint.py
Original file line number Diff line number Diff line change
Expand Up @@ -1940,9 +1940,6 @@ def pep_code_check_hint(hint: object) -> Tuple[str, bool, Tuple[str, ...]]:
hint_root = hint
del hint

# Human-readable label describing the root hint in exception messages.
hint_root_label = f'{EXCEPTION_CACHED_PLACEHOLDER}'

# Python code snippet evaluating to the current passed parameter or return
# value to be type-checked against the root hint.
pith_root_expr = PEP_CODE_PITH_ROOT_NAME
Expand All @@ -1959,15 +1956,6 @@ def pep_code_check_hint(hint: object) -> Tuple[str, bool, Tuple[str, ...]]:
# type) associated with the currently visited type hint if any.
hint_curr_expr = None

# Human-readable label prefixing the machine-readable representation of the
# currently visited type hint in exception and warning messages.
hint_curr_label = None

# Human-readable label prefixing the machine-readable representation of the
# currently visited type hint if this hint is nested (i.e., any hint
# *except* the root type hint) in exception and warning messages.
hint_curr_label_nested = f'{hint_root_label} {repr(hint_root)} child'

#FIXME: Excise us up.
# Origin type (i.e., non-"typing" superclass suitable for shallowly
# type-checking the current pith against the currently visited hint by
Expand Down Expand Up @@ -2154,6 +2142,23 @@ def pep_code_check_hint(hint: object) -> Tuple[str, bool, Tuple[str, ...]]:
# of that assignment expression.
pith_curr_assigned_expr: str = None # type: ignore[assignment]

# ..................{ HINT ~ label }..................
# Human-readable label describing the root hint in exception messages.
#
# Note that the "hint_curr_label" should almost *ALWAYS* be used instead.
HINT_ROOT_LABEL = EXCEPTION_CACHED_PLACEHOLDER

# Human-readable label prefixing the machine-readable representation of the
# currently visited type hint if this hint is nested (i.e., any hint
# *except* the root type hint) in exception and warning messages.
#
# Note that the "hint_curr_label" should almost *ALWAYS* be used instead.
HINT_CHILD_LABEL = f'{HINT_ROOT_LABEL} {repr(hint_root)} child'

# Human-readable label prefixing the machine-readable representation of the
# currently visited type hint in exception and warning messages.
hint_curr_label = None

# ..................{ METADATA }..................
# Tuple of metadata describing the currently visited hint, appended by
# the previously visited parent hint to the "hints_meta" stack.
Expand Down Expand Up @@ -2318,6 +2323,15 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
pith_curr_expr = hint_curr_meta[_HINT_META_INDEX_PITH_EXPR]
indent_curr = hint_curr_meta[_HINT_META_INDEX_INDENT]

#FIXME: This test can be trivially avoided by:
#* Initializing "hint_curr_label = HINT_ROOT_LABEL" above.
#* Unconditionally setting "hint_curr_label = HINT_CHILD_LABEL"
# below at the end of each iteration of this loop.
#
#Since we're going to be fundamentally refactoring this entire
#algorithm into a two-phase algorithm, let's hold off on that until the
#radioactive dust settles, shall we?

# Human-readable label prefixing the machine-readable representation of
# the currently visited type hint in exception and warning messages.
#
Expand All @@ -2327,9 +2341,9 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# from the former to the latter. The latter approach would be
# non-human-readable and insane.
hint_curr_label = (
hint_root_label
HINT_ROOT_LABEL
if hints_meta_index_curr == 0 else
hint_curr_label_nested
HINT_CHILD_LABEL
)

# ................{ REDUCTION }................
Expand Down Expand Up @@ -3427,7 +3441,7 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# absolutely should have... but may not have, which is why we're testing.
if func_code == func_root_code:
raise BeartypeDecorHintPepException(
f'{hint_root_label} {repr(hint_root)} not type-checked.')
f'{HINT_ROOT_LABEL} {repr(hint_root)} not type-checked.')
# Else, the breadth-first search above successfully generated code.

# Suffix this code by a Python code snippet raising a human-readable
Expand Down
12 changes: 6 additions & 6 deletions beartype/_util/cls/utilclstest.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
# ....................{ VALIDATORS }....................
def die_unless_type(
# Mandatory parameters.
cls: type,
cls: object,

# Optional parameters.
exception_cls: Type[Exception] = _BeartypeUtilTypeException,
Expand All @@ -44,7 +44,7 @@ def die_unless_type(
Parameters
----------
cls : type
cls : object
Object to be validated.
exception_cls : Type[Exception]
Type of exception to be raised. Defaults to
Expand Down Expand Up @@ -90,7 +90,7 @@ def is_type_builtin(cls: type) -> bool:

# Avoid circular import dependencies.
from beartype._util.py.utilpymodule import (
get_object_class_module_name_or_none)
get_object_type_module_name_or_none)

# If this object is *NOT* a class, raise an exception.
die_unless_type(cls)
Expand Down Expand Up @@ -120,7 +120,7 @@ def is_type_builtin(cls: type) -> bool:
# Fully-qualified name of the module defining this class if this class is
# defined by a module *OR* "None" otherwise (i.e., if this class is
# dynamically defined in-memory).
cls_module_name = get_object_class_module_name_or_none(cls)
cls_module_name = get_object_type_module_name_or_none(cls)

# This return true only if this name is that of the "builtins" module
# declaring all builtin classes.
Expand Down Expand Up @@ -158,7 +158,7 @@ def is_classname_builtin(classname: str) -> bool:
)

# ....................{ TESTERS ~ isinstanceable }....................
def is_type_isinstanceable(cls: type) -> bool:
def is_type_isinstanceable(cls: object) -> bool:
'''
``True`` only if the passed type cls is an **isinstanceable class** (i.e.,
class whose metaclass does *not* define an ``__instancecheck__()`` dunder
Expand Down Expand Up @@ -188,7 +188,7 @@ class whose metaclass does *not* define an ``__instancecheck__()`` dunder
Parameters
----------
cls : type
cls : object
Object to be tested.
Returns
Expand Down

0 comments on commit e25cde7

Please sign in to comment.