Skip to content

Commit

Permalink
Non-self-cached type hint caching x 3.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain internally caching
**non-self-cached type hints** (i.e., hints that do *not* internally
cache themselves somewhere like PEP 563- or 585-compliant type hints)
and coercing semantically equal non-self-cached type hints into
syntactically equal `@beartype`-cached type hints, dramatically
improving both the space and time efficiency of such hints.
Specifically, this commit documents an unresolved issue in which a
generic under Python >= 3.9 is used to annotate one or more callables in
both subscripted and unsubscripted forms (so, a crazy edge-case unlikely
to ever manifest in physical reality, but nonetheless an unresolved
issue) as well as reducing the previously defined private
`beartype._util.hint.pep.utilhintpeptest.is_hint_pep_uncached` tester to
an efficient `O(1)` test. (*Lousy lake trout with unslaked clout!*)
  • Loading branch information
leycec committed Feb 24, 2021
1 parent 0499052 commit ffc233f
Show file tree
Hide file tree
Showing 16 changed files with 291 additions and 148 deletions.
29 changes: 27 additions & 2 deletions beartype/_decor/_cache/cachetype.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,28 @@ def register_typistry_forwardref(hint_classname: str) -> str:
)

# ....................{ REGISTRARS ~ type }....................
#FIXME: Generalize this function to correctly distinguish between subscripted
#and subscripted generics under Python >= 3.9. For example, given a PEP
#585-compliant generic "class MuhList(list): pass", support both:
#* "MuhList", an unsubscripted generic.
#* "MuhList[int]", a subscripted generic.
#
#The core issue is that both of those type hints share the same classname
#despite being different objects and thus effectively different classes, which
#is all manner of screwball but something we nonetheless should probably
#support. We say "should," because it might very well be the responsibility of
#the parent pep_code_check_hint() function to strip the subscription from this
#generic if subscripted.
#
#In any case, this function *MUST* absolutely be improved to detect when this
#hint is subscripted (i.e., when "getattr(hint, '__args__', None) is not None")
#and correctly do something, which could be either:
#* Raising an exception.
#* Reducing this subscripted generic to its original unsubscripted class
# (i.e., "hint.__origin").
#
#Trivial in either case, but worth consideration as to which is preferable.

@callable_cached
def register_typistry_type(hint: type) -> str:
'''
Expand Down Expand Up @@ -418,7 +440,7 @@ def __setitem__(self, hint_name: str, hint: object) -> None:
Parameters
----------
hint_name: str : str
hint_name: str
String uniquely identifying this hint in a manner dependent on the
type of this hint. Specifically, if this hint is:
Expand All @@ -431,13 +453,16 @@ def __setitem__(self, hint_name: str, hint: object) -> None:
classnames.
* Hash of these types (ignoring duplicate types and type order in
this tuple).
hint : object
PEP-noncompliant type hint to be mapped from this string.
Raises
----------
TypeError
If this hint 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.
like dictionaries and sets). Since *all* supported PEP-noncompliant
type hints are hashable, this exception should *never* be raised.
_BeartypeDecorBeartypistryException
If either:
Expand Down
22 changes: 22 additions & 0 deletions beartype/_decor/_code/codemain.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,12 +402,34 @@
#with no return type hints *OR* deeply ignorable type hints. Why? Because we
#can trivially eliminate the additional stack frame in this edge case by
#unconditionally prefixing the body of the decorated callable by (in order):
#
#1. Code type-checking parameters passed to that callable.
#2. Code deleting *ALL* beartype-specific "__bear"-prefixed locals and globals
# referenced by the code type-checking those parameters. This is essential,
# as it implies that we then no longer need to iteratively search the body of
# the decorated callable for local variables with conflicting names, which
# due to strings we can't reliably do without "ast"- or "dis"-style parsing.
#
#Note this edge case only applies to callables:
#* Whose return hint is either:
# * Unspecified.
# * Deeply ignorable.
# * "None", implying this callable to return nothing. Callables explicitly
# returning a "None" value should instead be annotated with a return hint of
# "beartype.cave.NoneType"; this edge case would *NOT* apply to those.
#* *DIRECTLY* decorated by @beartype: e.g.,
# @beartype
# def muh_func(): pass
# This edge case does *NOT* apply to callables directly decorated by another
# decorator first, as in that case the above procedure would erroneously
# discard the dynamic decoration of that other decorator: e.g.,
# @beartype
# @other_decorator
# def wat_func(): pass
#* *NOT* implicitly transformed by one or more other import hooks. If any other
# import hooks are in effect, this edge case does *NOT* apply, as in that case
# the above procedure could again erroneously discard the dynamic
# transformations applied by those other import hooks.
#FIXME: *GENERALIZATION:* All of the above would seem to pertain to a
#prospective higher-level package, which has yet to be officially named but
#which we are simply referring to as "beartypecache" for now. "beartypecache"
Expand Down
7 changes: 2 additions & 5 deletions beartype/_util/func/utilfuncget.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
# See "LICENSE" for further details.

'''
**Beartype callable getter utilities.**
This private submodule implements utility functions dynamically introspecting
high-level metadata attached to arbitrary callables.
Package-wide **callable utility getters** (i.e., callables dynamically
introspecting metadata attached to arbitrary callables).
This private submodule is *not* intended for importation by downstream callers.
'''
Expand All @@ -17,7 +15,6 @@
from collections.abc import Callable

# ....................{ GETTERS }....................
#FIXME: Unit test us up.
def get_func_wrappee(func: Callable) -> Callable:
'''
**Wrappee** (i.e., lower-level callable) originally wrapped by the passed
Expand Down
37 changes: 1 addition & 36 deletions beartype/_util/hint/pep/proposal/utilhintpep585.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from beartype._util.cache.utilcachecall import callable_cached
from beartype._util.func.utilfuncget import get_func_wrappee
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_9
from typing import Any, Set, Tuple, TypeVar
from typing import Any, Set, Tuple

# See the "beartype.cave" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']
Expand Down Expand Up @@ -94,11 +94,6 @@ def is_hint_pep585_generic(hint: object) -> bool:
for hint_base_erased in hint_bases_erased
)


# Unmemoized wrappee wrapped by the above memoized wrapper, for use in
# edge cases where memoization is actually undesirable.
is_hint_pep585_generic_uncached = get_func_wrappee(is_hint_pep585_generic)

# Else, the active Python interpreter targets at most Python < 3.9 and thus
# fails to support PEP 585. In this case, fallback to declaring this function
# to unconditionally return False.
Expand All @@ -109,8 +104,6 @@ def is_hint_pep585(hint: object) -> bool:
def is_hint_pep585_generic(hint: object) -> bool:
return False

is_hint_pep585_generic_uncached = is_hint_pep585_generic

# ....................{ TESTERS ~ doc }....................
# Docstring for this function regardless of implementation details.
# Docstring for this function regardless of implementation details.
Expand Down Expand Up @@ -178,34 +171,6 @@ def is_hint_pep585_generic(hint: object) -> bool:
https://www.python.org/dev/peps/pep-0585
'''


is_hint_pep585_generic_uncached.__doc__ = '''
``True`` only if the passed possibly non-cached object is a `PEP
585`_-compliant **generic** (i.e., class superficially subclassing at least
one subscripted `PEP 585`_-compliant pseudo-superclass).
Caveats
-------
**This non-memoized function should only be called at a sufficiently early
time during** :mod:`beartype` **decoration,** when the passed object cannot
be guaranteed to have already been cached somewhere. Since this is
typically *not* the case, the memoized :func:`is_hint_pep585_generic`
function wrapping this function should typically be called instead.
Parameters
----------
hint : object
Possibly non-cached object to be inspected.
Returns
----------
bool
``True`` only if this object is a `PEP 585`_-compliant generic.
.. _PEP 585:
https://www.python.org/dev/peps/pep-0585
'''

# ....................{ GETTERS }....................
def get_hint_pep585_generic_bases_unerased(hint: Any) -> tuple:
'''
Expand Down
102 changes: 32 additions & 70 deletions beartype/_util/hint/pep/utilhintpeptest.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,13 @@
from beartype._util.hint.pep.proposal.utilhintpep585 import (
is_hint_pep585,
is_hint_pep585_generic,
is_hint_pep585_generic_uncached,
)
from beartype._util.hint.pep.proposal.utilhintpep593 import (
is_hint_pep593_ignorable_or_none)
from beartype._util.utilobject import get_object_class_unless_class
from beartype._util.py.utilpymodule import get_object_module_name
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_7
from typing import TypeVar, Optional
from typing import TypeVar
from warnings import warn

# See the "beartype.cave" submodule for further commentary.
Expand Down Expand Up @@ -318,8 +317,8 @@ def die_if_hint_pep_sign_unsupported(
f'currently unsupported by @beartype.')

# ....................{ WARNINGS }....................
#FIXME: Resurrect usage the passed "hint_label" parameter. We've currently
#disabled this parameter as it's typically just a non-human-readable
#FIXME: Resurrect support for the passed "hint_label" parameter. We've
#currently disabled this parameter as it's typically just a non-human-readable
#placeholder substring *NOT* intended to be exposed to end users (e.g.,
#"$%ROOT_PITH_LABEL/~"). For exceptions, we simply catch raised exceptions and
#replace such substrings with human-readable equivalents. Can we perform a
Expand Down Expand Up @@ -556,15 +555,16 @@ def is_hint_pep_uncached(hint: object) -> bool:
'''
``True`` only if the passed object is a **PEP-compliant non-self-cached
type hint** (i.e., PEP-compliant type hint that does *not* externally cache
itself).
itself somewhere).
Since most PEP-compliant type hints are self-caching, this function
returns:
* ``True`` for only:
* `PEP 484`_-compliant generics (e.g., ``from typing import Generic;
class MuhPep484List(Generic): pass; MuhPep484List[int]``).
* `PEP 484`_-compliant subscripted generics under Python >= 3.9 (e.g.,
``from typing import List; class MuhPep484List(List): pass;
MuhPep484List[int]``). See below for further commentary.
* `PEP 585`_-compliant type hints, including both:
* Builtin `PEP 585`_-compliant type hints (e.g., ``list[int]``).
Expand All @@ -573,13 +573,28 @@ class MuhPep484List(Generic): pass; MuhPep484List[int]``).
* ``False`` for *all* other PEP-compliant type hints.
Caveats
----------
This function *cannot* be meaningfully memoized, since the passed type hint
is *not* guaranteed to be cached somewhere. Only functions passed cached
type hints can be meaningfully memoized. Since this high-level function
internally defers to unmemoized low-level functions that are ``O(n)`` in
``n`` the size of the inheritance hierarchy , this function should be
called sparingly. See the
:mod:`beartype._decor._cache.cachehint` submodule for further details.
``n`` the size of the inheritance hierarchy of this hint, this function
should be called sparingly. See the :mod:`beartype._decor._cache.cachehint`
submodule for further details.
This tester intentionally returns a false negative for `PEP 484`_-compliant
generics subscripted by type variables under Python < 3.9. Although those
hints are technically non-self-cached, this tester falsely reports those
hints to be self-cached by returning ``False``. Why? Because correctly
detecting those hints as non-self-cached would require an unmemoized
``O(n)`` search across the inheritance hierarchy of *all* passed objects
and thus all type hints annotating callables decorated by
:func:`beartype.beartype`. Since this failure only affects obsolete Python
versions *and* since the only harms induced by this failure are a slight
increase in space and time consumption for edge-case type hints unlikely to
actually be used in real-world code, this tradeoff is more than acceptable.
We're not the bad guy here. Right?
Parameters
----------
Expand All @@ -597,19 +612,11 @@ class MuhPep484List(Generic): pass; MuhPep484List[int]``).
https://www.python.org/dev/peps/pep-0585
'''

# Return true only if either...
#
# Note that these tests are intentionally ordered in descending likelihood
# of a match with the least common type hints tested last and the most
# common type hints tested first.
return (
# This hint is a PEP 585-compliant type hint.
is_hint_pep585(hint) or
# This hint is a PEP-compliant generic, tested in a safe non-memoized
# manner explicitly preventing this hint from being unintentionally
# cached in a place where it *CANNOT* be readily looked up from.
is_hint_pep_generic_uncached(hint)
)
# Return true only if this hint is either:
# * A PEP 585-compliant type hint.
# * A PEP 484-compliant subscripted generic masquerading as a PEP
# 585-compliant type hint. *shrug*
return is_hint_pep585(hint)

# ....................{ TESTERS ~ ignorable }....................
def is_hint_pep_ignorable(hint: object) -> bool:
Expand Down Expand Up @@ -932,7 +939,8 @@ def is_hint_pep_class_typing(hint: object) -> bool:
def is_hint_pep_generic(hint: object) -> bool:
'''
``True`` only if the passed object is a **generic** (i.e., class
superficially subclassing at least one non-class PEP-compliant object).
superficially subclassing at least one PEP-compliant type hint that is
possibly *not* an actual class).
Specifically, this tester returns ``True`` only if this object is a class
that is either:
Expand Down Expand Up @@ -981,52 +989,6 @@ def is_hint_pep_generic(hint: object) -> bool:
)
)


#FIXME: Unit test us up, please.
def is_hint_pep_generic_uncached(hint: object) -> bool:
'''
``True`` only if the passed possibly non-cached object is a **generic**
(i.e., class superficially subclassing at least one non-class PEP-compliant
object).
Caveats
-------
**This non-memoized function should only be called at a sufficiently early
time during** :mod:`beartype` **decoration,** when the passed object cannot
be guaranteed to have already been cached somewhere. Since this is
typically *not* the case, the memoized :func:`is_hint_pep585_generic`
function wrapping this function should typically be called instead.
Parameters
----------
hint : object
Possibly non-cached object to be inspected.
Returns
----------
bool
``True`` only if this object is a generic.
See Also
----------
:func:`is_hint_pep_generic`
Further details.
'''

# Return true only if this hint is...
return (
# A class that is either...
isinstance(hint, type) and (
# A PEP 484-compliant generic. Note this test trivially reduces to
# an O(1) operation and is thus tested first.
is_hint_pep484_generic(hint) or
# A PEP 585-compliant generic. Note this test is O(n) in n the
# number of pseudo-superclasses originally subclassed by this
# generic and is thus tested last.
is_hint_pep585_generic_uncached(hint)
)
)

# ....................{ TESTERS ~ subtype : tuple }....................
def is_hint_pep_tuple_empty(hint: object) -> bool:
'''
Expand Down
2 changes: 1 addition & 1 deletion beartype/_util/py/utilpyinterpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# See "LICENSE" for further details.

'''
Project-wide **Python interpreter utilities.**
Package-wide **Python interpreter utilities.**
This private submodule is *not* intended for importation by downstream callers.
'''
Expand Down
2 changes: 1 addition & 1 deletion beartype/_util/py/utilpymodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# See "LICENSE" for further details.

'''
Project-wide **Python module** utilities.
Package-wide **Python module utilities**.
This private submodule is *not* intended for importation by downstream callers.
'''
Expand Down
2 changes: 1 addition & 1 deletion beartype/_util/py/utilpyversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# See "LICENSE" for further details.

'''
Project-wide **Python interpreter version** utilities.
Package-wide **Python interpreter version utilities**.
This private submodule is *not* intended for importation by downstream callers.
'''
Expand Down
2 changes: 1 addition & 1 deletion beartype/_util/text/utiltextlabel.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# See "LICENSE" for further details.

'''
Project-wide **text label utilities** (i.e., callables generating
Package-wide **text label utilities** (i.e., callables generating
human-readable strings describing prominent objects or types, which are then
typically interpolated into exception messages).
Expand Down

0 comments on commit ffc233f

Please sign in to comment.