Skip to content

Commit

Permalink
Exception handling O(n) -> O(1) x 2.
Browse files Browse the repository at this point in the history
This commit is the last in a commit chain fundamentally refactoring
the private `beartype._decor._code._pep._error` subpackage responsible
for raising human-readable exceptions in the event of type-checking
violations from a linear- to constant-time algorithm, while still
preserving optional support for linear-time behaviour in anticipation of
permitting end users to conditionally enable linear-time type-checking
under some subsequent stable release of `beartype`. Specifically, this
commit refactors our exception handler to conditionally switch between
O(1) and O(n) behaviour as required by the parent `@beartype`-decorated
wrapper function.

## Features Optimized

* **`O(n)` → `O(1)` exception handling.** `@beartype` now internally
  raises human-readable exceptions in the event of type-checking
  violations with an `O(1)` rather than `O(n)` algorithm, significantly
  reducing time complexity for the edge case of invalid large sequences
  either passed to or returned from `@beartype`-decorated callables. For
  forward compatibility with a future version of `beartype` enabling
  users to explicitly switch between constant- and linear-time checking,
  the prior `O(n)` exception-handling algorithm has been preserved in a
  presently disabled form.

(*Standard deviation of an enviable discombobulation!*)
  • Loading branch information
leycec committed Feb 2, 2021
1 parent e65626c commit 4687983
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 88 deletions.
26 changes: 26 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2512,6 +2512,30 @@ Let's take this from the top.
#. **Make changes to this branch** in your favourite `Integrated Development
Environment (IDE) <IDE_>`__. Of course, this means Vim_.
#. **Test these changes.** Note this command assumes you have installed *all*
`major versions of both CPython and PyPy supported by the next stable
release of beartype you are hacking on <Features_>`__. If this is *not* the
case, install these versions with pyenv_. This is vital, as type hinting
support varies significantly between major versions of different Python
interpreters.

.. code-block:: shell-session
tox
The resulting output should ideally be suffixed by a synopsis resembling:

::

________________________________ summary _______________________________
py36: commands succeeded
py37: commands succeeded
py38: commands succeeded
py39: commands succeeded
pypy36: commands succeeded
pypy37: commands succeeded
congratulations :)

#. **Stage these changes.**

.. code-block:: shell-session
Expand Down Expand Up @@ -2983,6 +3007,8 @@ application stack at tool rather than Python runtime) include:
https://www.sphinx-doc.org/en/master
.. _SymPy:
https://www.sympy.org
.. _pyenv:
https://operatingops.org/2020/10/24/tox-testing-multiple-python-versions-with-pyenv

.. # ------------------( LINKS ~ py : package : numpy )------------------
.. _NumPy:
Expand Down
138 changes: 94 additions & 44 deletions beartype/_decor/_code/_pep/_error/_peperrorsequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ def get_cause_or_none_sequence_standard(
'''
assert isinstance(sleuth, CauseSleuth), f'{repr(sleuth)} not cause sleuth.'
assert sleuth.hint_sign in HINT_PEP_SIGNS_SEQUENCE_STANDARD, (
f'{repr(sleuth.hint)} not standard sequence.')
f'{repr(sleuth.hint)} not standard sequence hint.')

# Assert this sequence was subscripted by exactly one argument. Note that
# the "typing" module should have already guaranteed this on our behalf.
assert len(sleuth.hint_childs) == 1, (
f'Standard sequence {repr(sleuth.hint)} subscripted by '
f'Standard sequence hint {repr(sleuth.hint)} subscripted by '
f'multiple arguments.')

# Non-"typing" class originating this attribute (e.g., "list" for "List").
Expand Down Expand Up @@ -86,16 +86,16 @@ def get_cause_or_none_tuple(sleuth: CauseSleuth) -> 'Optional[str]':
'''
assert isinstance(sleuth, CauseSleuth), f'{repr(sleuth)} not cause sleuth.'
assert sleuth.hint_sign in HINT_PEP_SIGNS_TUPLE, (
f'{repr(sleuth.hint_sign)} not tuple.')
f'{repr(sleuth.hint_sign)} not tuple hint.')

# If this pith is *NOT* an instance of this class, defer to the getter
# function handling non-"typing" classes.
if not isinstance(sleuth.pith, tuple):
return get_cause_or_none_type(sleuth.permute(hint=tuple))

# If this hint is...
# If this hint is a tuple...
if (
# This tuple is subscripted by exactly two child hints *AND*...
# Subscripted by exactly two child hints *AND*...
len(sleuth.hint_childs) == 2 and
# The second child hint is just an unquoted ellipsis...
sleuth.hint_childs[1] is Ellipsis
Expand Down Expand Up @@ -172,58 +172,108 @@ def get_cause_or_none_tuple(sleuth: CauseSleuth) -> 'Optional[str]':
def _get_cause_or_none_sequence(sleuth: CauseSleuth) -> 'Optional[str]':
'''
Human-readable string describing the failure of the passed arbitrary object
to satisfy the passed **PEP-compliant possibly non-standard sequence type
hint** (i.e., PEP-compliant type hint accepting one or more subscripted
type hint arguments constraining *all* items of this object, which
necessarily satisfies the :class:`collections.abc.Sequence` protocol with
guaranteed ``O(1)`` indexation across all sequence items) if this object
actually fails to satisfy this hint *or* ``None`` otherwise (i.e., if this
object satisfies this hint).
to satisfy the passed **PEP-compliant variadic sequence type hint** (i.e.,
PEP-compliant type hint accepting one or more subscripted type hint
arguments constraining *all* items of this object, which necessarily
satisfies the :class:`collections.abc.Sequence` protocol with guaranteed
``O(1)`` indexation across all sequence items) if this object actually
fails to satisfy this hint *or* ``None`` otherwise (i.e., if this object
satisfies this hint).
Parameters
----------
sleuth : CauseSleuth
Type-checking error cause sleuth.
'''
# Assert this type hint to describe a variadic sequence. See the parent
# get_cause_or_none_sequence_standard() and get_cause_or_none_tuple()
# functions for derivative logic.
#
# Note that this pith need *NOT* be validated to be an instance of the
# expected variadic sequence, as the caller guarantees this to be the case.
assert isinstance(sleuth, CauseSleuth), f'{repr(sleuth)} not cause sleuth.'
assert (
sleuth.hint_sign in HINT_PEP_SIGNS_SEQUENCE_STANDARD or (
sleuth.hint_sign in HINT_PEP_SIGNS_TUPLE and
len(sleuth.hint_childs) == 2 and
sleuth.hint_childs[1] is Ellipsis
)
), f'{repr(sleuth.hint)} neither standard sequence nor variadic tuple.'

# First child hint of this hint. All remaining child hints if any are
# ignorable. Specifically, if this hint is:
# * A standard sequence (e.g., "typing.List[str]"), this hint is
# subscripted by only one child hint.
# * A variadic tuple (e.g., "typing.Tuple[str, ...]"), this hint is
# subscripted by only two child hints the latter of which is ignorable
# syntactic chuff.
hint_child = sleuth.hint_childs[0]

# If this child hint is *NOT* ignorable...
if not is_hint_ignorable(hint_child):
# For each enumerated item of this pith...
for pith_item_index, pith_item in enumerate(sleuth.pith):
# Human-readable string describing the failure of this item to
# satisfy this child hint if this item actually fails to satisfy
# this child hint *or* "None" otherwise.
pith_item_cause = sleuth.permute(
pith=pith_item, hint=hint_child).get_cause_or_none()
), (f'{repr(sleuth.hint)} neither '
f'standard sequence nor variadic tuple hint.')

# If this item is the cause of this failure, return a substring
# describing this failure by embedding this failure (itself
# intended to be embedded in a longer string).
if pith_item_cause is not None:
return (
f'{sleuth.pith.__class__.__name__} item '
f'{pith_item_index} {pith_item_cause}')
# Else, this item is *NOT* the cause of this failure. Silently
# continue to the next.
# Else, this child hint is ignorable.
# If this sequence is non-empty...
if sleuth.pith:
# First child hint of this hint. All remaining child hints if any are
# ignorable. Specifically, if this hint is:
# * A standard sequence (e.g., "typing.List[str]"), this hint is
# subscripted by only one child hint.
# * A variadic tuple (e.g., "typing.Tuple[str, ...]"), this hint is
# subscripted by only two child hints the latter of which is
# ignorable syntactic chuff.
hint_child = sleuth.hint_childs[0]

# If this child hint is *NOT* ignorable...
if not is_hint_ignorable(hint_child):
# Arbitrary iterator satisfying the enumerate() protocol, yielding
# zero or more 2-tuples of the form "(item_index, item)", where:
# * "item" is an arbitrary item of this sequence.
# * "item_index" is the 0-based index of this item.
pith_enumerator = None

# If this sequence was indexed by the parent @beartype-generated
# wrapper function by a pseudo-random integer in O(1) time,
# type-check *ONLY* the same index of this sequence also in O(1)
# time. Since the current call to that function failed a
# type-check, either this index is the index responsible for that
# failure *OR* this sequence is valid and another container
# entirely is responsible for that failure. In either case, no
# other indices of this sequence need be checked.
if sleuth.random_int is not None:
# 0-based index of this item calculated from this random
# integer in the *SAME EXACT WAY* as in the parent
# @beartype-generated wrapper function.
pith_item_index = sleuth.random_int % len(sleuth.pith)

# Pseudo-random item with this index in this sequence.
pith_item = sleuth.pith[pith_item_index]

# 2-tuple of this index and item in the same order as the
# 2-tuples returned by the enumerate() builtin.
pith_enumeratable = (pith_item_index, pith_item)

# Iterator yielding only this 2-tuple.
pith_enumerator = iter((pith_enumeratable,))
# print(f'Checking item {pith_item_index} in O(1) time!')
# Else, this sequence was iterated by the parent
# @beartype-generated wrapper function in O(n) time. In this case,
# type-check *ALL* indices of this sequence in O(n) time as well.
else:
# Iterator yielding all indices and items of this sequence.
pith_enumerator = enumerate(sleuth.pith)
# print('Checking sequence in O(n) time!')

# For each enumerated item of this (sub)sequence...
for pith_item_index, pith_item in pith_enumerator:
# Human-readable string describing the failure of this item
# to satisfy this child hint if this item actually fails to
# satisfy this child hint *or* "None" otherwise.
pith_item_cause = sleuth.permute(
pith=pith_item, hint=hint_child).get_cause_or_none()

# If this item is the cause of this failure, return a
# substring describing this failure by embedding this
# failure (itself intended to be embedded in a longer
# string).
if pith_item_cause is not None:
return (
f'{sleuth.pith.__class__.__name__} item '
f'{pith_item_index} {pith_item_cause}')
# Else, this item is *NOT* the cause of this failure.
# Silently continue to the next.
# Else, this child hint is ignorable.
# Else, this sequence is empty, in which case all items of this sequence
# (of which there are none) are valid. Just go with it, people.

# Return "None", as all items of this pith are valid, implying this pith to
# deeply satisfy this hint.
# Return "None", as all items of this sequence are valid, implying this
# sequence to deeply satisfy this hint.
return None
21 changes: 20 additions & 1 deletion beartype/_decor/_code/_pep/_error/_peperrorsleuth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
'''

# ....................{ IMPORTS }....................
from beartype.cave import NoneType
from beartype.cave import NoneType, NoneTypeOr
from beartype.roar import _BeartypeCallHintPepRaiseException
from beartype._util.hint.pep.proposal.utilhintpep484 import (
get_hint_pep484_newtype_class,
Expand Down Expand Up @@ -88,6 +88,15 @@ class CauseSleuth(object):
* Else, ``None``.
pith : object
Arbitrary object to be validated.
random_int: Optional[int]
**Pseudo-random integer** (i.e., unsigned 32-bit integer
pseudo-randomly generated by the parent :func:`beartype.beartype`
wrapper function in type-checking randomly indexed container items by
the current call to that function) if that function generated such an
integer *or* ``None`` otherwise (i.e., if that function generated *no*
such integer). See the same parameter accepted by the higher-level
:func:`beartype._decor._code._pep._error.peperror.raise_pep_call_exception`
function for further details.
'''

# ..................{ CLASS VARIABLES }..................
Expand All @@ -102,6 +111,7 @@ class CauseSleuth(object):
'hint_sign',
'hint_childs',
'pith',
'random_int',
)


Expand All @@ -111,6 +121,7 @@ class CauseSleuth(object):
'func',
'hint',
'pith',
'random_int',
))
'''
Frozen set of the names of all parameters accepted by the :meth:`init`
Expand All @@ -125,13 +136,18 @@ class CauseSleuth(object):
'''

# ..................{ INITIALIZERS }..................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# CAUTION: Whenever adding, deleting, or renaming any parameter accepted by
# this method, make similar changes to the "_INIT_PARAM_NAMES" set above.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
def __init__(
self,
func: object,
pith: object,
hint: object,
cause_indent: str,
exception_label: str,
random_int: int,
) -> None:
'''
Initialize this object.
Expand All @@ -141,13 +157,16 @@ def __init__(
f'{repr(cause_indent)} not string.')
assert isinstance(exception_label, str), (
f'{repr(exception_label)} not string.')
assert isinstance(random_int, NoneTypeOr[int]), (
f'{repr(random_int)} not integer or "None".')

# Classify all passed parameters.
self.func = func
self.pith = pith
self.hint = hint
self.cause_indent = cause_indent
self.exception_label = exception_label
self.random_int = random_int

# Nullify all remaining parameters for safety.
self.hint_sign = None
Expand Down
37 changes: 37 additions & 0 deletions beartype/_decor/_code/_pep/_error/peperror.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,42 @@
# *ONLY* the sole index of the current container indicated by that
# parameter.
# * Else, continue to perform O(n)-style type-checking as we currently do.
#FIXME: Generalizing the "random_int" concept (i.e., the optional "random_int"
#parameter accepted by the raise_pep_call_exception() function) that enables
#O(1) rather than O(n) exception handling to containers that do *NOT* provide
#efficient random access like mappings and sets will be highly non-trivial.
#While there exist a number of alternative means of implementing that
#generalization, the most reasonable *BY FAR* is probably to:
#
#* Embed additional assignment expressions in the type-checking tests generated
# by the pep_code_check_hint() function that uniquely store the value of each
# item, key, or value returned by each access of a non-indexable container
# iterator into a new unique local variable. Note this unavoidably requires:
# * Adding a new index to the ".hint_curr_meta" tuples internally created by
# that function -- named, say, "_HINT_META_INDEX_ITERATOR_VALUE". The value
# of the tuple item at this index should either be:
# * If the currently iterated type hint is a non-indexable container, the
# name of the new unique local variable assigned to by this assignment
# expression whose value is obtained from the iterator cached for that
# container.
# * Else, "None".
# * Python >= 3.8, but that's largely fine. Python 3.6 and 3.7 are
# increasingly obsolete in 2021.
#* Add a new optional "iterator_values" parameter to the
# raise_pep_call_exception() function, accepting either:
# * If the current parameter or return of the parent wrapper function was
# annotated with one or more non-indexable container type hints, a *LIST* of
# the *VALUES* of all unique local variables assigned to by assignment
# expressions in that parent wrapper function. These values were obtained
# from the iterators cached for those containers. To enable these exception
# handlers to efficiently treat this list like a FIFO stack (e.g., with the
# list.pop() method), this list should be sorted in the reverse order that
# these assignment expressions are defined in.
#* Refactor exception handlers to then preferentially retrieve non-indexable
# container items in O(1) time from this stack rather than simply iterating
# over all container items in O(n) brute-force time. Obviously, extreme care
# must be taken here to ensure that this exception handling algorithm visits
# containers in the exact same order as visited by our testing algorithm.

# ....................{ IMPORTS }....................
from beartype.cave import NoneTypeOr
Expand Down Expand Up @@ -244,6 +280,7 @@ def raise_pep_call_exception(
hint=hint,
cause_indent='',
exception_label=pith_label,
random_int=random_int,
).get_cause_or_none()

# If this pith does *NOT* satisfy this hint...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -574,8 +574,9 @@ def __len__(self) -> bool:
pith=[73,],
# Match that the exception message raised for this object...
exception_str_match_regexes=(
# Declares the index of this list's problematic item.
r'\s[Ll]ist item 0\s',
# Declares the index of a random list item *NOT*
# satisfying this hint.
r'\s[Ll]ist item \d+\s',
# Double-quotes the value of this item.
r'\s"73"\s',
),
Expand Down Expand Up @@ -745,8 +746,8 @@ def __len__(self) -> bool:
),
# Match that the exception message raised for this object...
exception_str_match_regexes=(
# Declares the index and expected type of this tuple's
# problematic item.
# Declares the index and expected type of a fixed
# tuple item *NOT* satisfying this hint.
r'\s[Tt]uple item 2\s',
r'\bstr\b',
),
Expand Down Expand Up @@ -796,9 +797,10 @@ def __len__(self) -> bool:
),
# Match that the exception message raised for this object...
exception_str_match_regexes=(
# Declares the index and expected type of this tuple's
# problematic item.
r'\s[Tt]uple item 0 tuple item 2\s',
# Declares the index and expected type of a random
# tuple item of a fixed tuple item *NOT* satisfying
# this hint.
r'\s[Tt]uple item \d+ tuple item 2\s',
r'\bstr\b',
),
),
Expand Down

0 comments on commit 4687983

Please sign in to comment.