Skip to content

Commit

Permalink
Non-self-cached type hint caching x 2.
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 defines a new private
`beartype._util.hint.pep.utilhintpeptest.is_hint_pep_uncached` tester
returning ``True`` only if the passed PEP-compliant type hint is
non-self-cached. Naturally, nothing is tested. Weep! (*Clouded endowment!*)
  • Loading branch information
leycec committed Feb 13, 2021
1 parent 871733e commit 76a4457
Show file tree
Hide file tree
Showing 10 changed files with 409 additions and 96 deletions.
226 changes: 175 additions & 51 deletions beartype/_decor/_cache/cachehint.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@
'''

# ....................{ IMPORTS }....................
from beartype._util.hint.utilhinttest import die_unless_hint
from collections.abc import Callable
from typing import Union

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

# ....................{ CACHES }....................
HINT_REPR_TO_HINT = {}
# ....................{ GLOBALS }....................
_HINT_REPR_TO_HINT = {}
'''
**Type hint cache** (i.e., singleton dictionary mapping from the
machine-readable representations of all non-self-cached type hints to those
Expand All @@ -39,35 +43,6 @@
* Type hints declared by the :mod:`typing` module, which implicitly cache
themselves on subscription thanks to inscrutable metaclass magic.
Design
------
This dictionary does *not* bother caching **self-cached type hints** (i.e.,
type hints that externally cache themselves), as these hints are already cached
elsewhere. Self-cached type hints include most `PEP 484`_-compliant type hints
declared by the :mod:`typing` module, which means that subscripting type hints
declared by the :mod:`typing` module with the same child type hints reuses the
exact same internally cached objects rather than creating new uncached objects:
e.g.,
.. code-block:: python
>>> import typing
>>> typing.List[int] is typing.List[int]
True
Equivalently, this dictionary *only* caches **non-self-cached type hints**
(i.e., type hints that do *not* externally cache themselves), as these hints
are *not* already cached elsewhere. Non-self-cached type hints include *all*
`PEP 585`_-compliant type hints produced by subscripting builtin container
types, which means that subscripting builtin container types with the same
child type hints creates new uncached objects rather than reusing the same
internally cached objects: e.g.,
.. code-block:: python
>>> list[int] is list[int]
False
Implementation
--------------
This dictionary is intentionally designed as a naive dictionary rather than
Expand All @@ -86,30 +61,179 @@
temporarily caching hints in an LRU cache is pointless, as there are *no*
space savings in dropping stale references to unused hints.
Motivation
----------
This dictionary enables callers to coerce non-self-cached type hints into
:mod:`beartype`-cached type hints. :mod:`beartype` effectively requires *all*
type hints to be cached somewhere! :mod:`beartype` does *not* care who, what,
or how is caching those type hints -- only that they are cached before being
passed to utility functions in the :mod:`beartype` codebase. Why? Because
most such utility functions are memoized for efficiency by the
:func:`beartype._util.cache.utilcachecall.callable_cached` decorator, which
maps passed parameters (typically including the standard ``hint`` parameter
accepting a type hint) based on object identity to previously cached return
values. You see the problem, we trust.
Non-self-cached type hints that are otherwise semantically equal are
nonetheless distinct objects and will thus be treated as distinct parameters by
memoization decorators. If this dictionary did *not* exist, non-self-cached
type hints could *not* be coerced into :mod:`beartype`-cached type hints and
thus could *not* be memoized, dramatically reducing the efficiency of
:mod:`beartype` for common type hints.
.. _PEP 484:
https://www.python.org/dev/peps/pep-0484
.. _PEP 563:
https://www.python.org/dev/peps/pep-0563
.. _PEP 585:
https://www.python.org/dev/peps/pep-0585
'''

# ....................{ CACHERS }....................
#FIXME: Replace all calls to coerce_hint_pep() with calls to this function.
def cache_hint_nonpep563(
func: Callable,
pith_name: str,
hint: object,
hint_label: str,
) -> object:
'''
Coerce and cache the passed (possibly non-self-cached and/or
PEP-noncompliant) type hint annotating the parameter or return value with
the passed name of the passed callable into the equivalent
:mod:`beartype`-cached PEP-compliant type hint if needed *or* silently
reduce to a noop otherwise (i.e., if this hint is already both self-cached
and PEP-compliant).
Specifically, this function (in order):
#. If the passed type hint is already self-cached, this hint is already
PEP-compliant by definition. In this case, this function preserves and
returns this hint as is.
#. Else if a semantically equivalent type hint (i.e., having the same
machine-readable representation) as this hint was already cached by a
prior call to this function, the current call to this function:
* Replaces this hint in the ``__annotations__`` dunder tuple of this
callable with this previously cached hint, minimizing memory space
consumption across the lifetime of the active Python process.
* Returns this previously cached hint.
#. Else if this hint is a **PEP-noncompliant tuple union** (i.e., tuple of
one or more standard classes and forward references to standard
classes), this function:
* Coerces this tuple union into the equivalent `PEP 484`_-compliant
union.
* Replaces this tuple union in the ``__annotations__`` dunder tuple of
this callable with this `PEP 484`_-compliant union.
#. Else (i.e., if this hint is neither PEP-compliant nor -noncompliant and
thus unsupported by :mod:`beartype`), this function raises an exception.
#. Internally caches this hint with the :data:`_HINT_REPR_TO_HINT`
dictionary.
#. Returns this hint.
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.
The ``_nonpep563`` substring suffixing the name of this function implies
this function is intended to be called *after* all possibly `PEP
563`_-compliant **deferred type hints** (i.e., type hints persisted as
evaluatable strings rather than actual type hints) annotating this callable
if any have been evaluated into actual type hints.
Design
------
This function does *not* bother caching **self-cached type hints** (i.e.,
type hints that externally cache themselves), as these hints are already
cached elsewhere. Self-cached type hints include most `PEP 484`_-compliant
type hints declared by the :mod:`typing` module, which means that
subscripting type hints declared by the :mod:`typing` module with the same
child type hints reuses the exact same internally cached objects rather
than creating new uncached objects: e.g.,
.. code-block:: python
>>> import typing
>>> typing.List[int] is typing.List[int]
True
Equivalently, this function *only* caches **non-self-cached type hints**
(i.e., type hints that do *not* externally cache themselves), as these
hints are *not* already cached elsewhere. Non-self-cached type hints
include *all* `PEP 585`_-compliant type hints produced by subscripting
builtin container types, which means that subscripting builtin container
types with the same child type hints creates new uncached objects rather
than reusing the same internally cached objects: e.g.,
.. code-block:: python
>>> list[int] is list[int]
False
Motivation
----------
This function enables callers to coerce non-self-cached type hints into
:mod:`beartype`-cached type hints. :mod:`beartype` effectively requires
*all* type hints to be cached somewhere! :mod:`beartype` does *not* care
who, what, or how is caching those type hints -- only that they are cached
before being passed to utility functions in the :mod:`beartype` codebase.
Why? Because most such utility functions are memoized for efficiency by the
:func:`beartype._util.cache.utilcachecall.callable_cached` decorator, which
maps passed parameters (typically including the standard ``hint`` parameter
accepting a type hint) based on object identity to previously cached return
values. You see the problem, we trust.
Non-self-cached type hints that are otherwise semantically equal are
nonetheless distinct objects and will thus be treated as distinct
parameters by memoization decorators. If this function did *not* exist,
non-self-cached type hints could *not* be coerced into
:mod:`beartype`-cached type hints and thus could *not* be memoized,
reducing the efficiency of :mod:`beartype` for standard type hints.
Parameters
----------
func : Callable
Callable annotated by this hint.
pith_name : str
Either:
* If this hint annotates a parameter, the name of that parameter.
* If this hint annotates the return, ``"return"``.
hint : object
Possibly non-self-cached and/or PEP-noncompliant type hint to be
coerced and cached into the equivalent :mod:`beartype`-cached
PEP-compliant type hint.
hint_label : str
Human-readable label describing this hint.
Returns
----------
object
Either:
* If this hint is either non-self-cached *or* PEP-noncompliant, the
equivalent :mod:`beartype`-cached PEP-compliant type hint coerced and
cached from this hint.
* If this hint is self-cached *and* PEP-compliant, this hint as is.
Raises
----------
BeartypeDecorHintNonPepException
If this object is neither:
* A PEP-noncompliant type hint.
* A supported PEP-compliant type hint.
.. _PEP 484:
https://www.python.org/dev/peps/pep-0484
.. _PEP 563:
https://www.python.org/dev/peps/pep-0563
.. _PEP 585:
https://www.python.org/dev/peps/pep-0585
'''

#FIXME: Call the new is_hint_pep_uncached() tester here to decide whether
#or not to cache this hint.

# If this hint is a PEP-noncompliant tuple union, coerce this union into
# the equivalent PEP-compliant union subscripted by the same child hints.
# By definition, PEP-compliant unions are a strict superset of
# PEP-noncompliant tuple unions and thus accept all child hints accepted by
# the latter.
if isinstance(hint, tuple):
assert callable(func), f'{repr(func)} not callable.'
assert isinstance(pith_name, str), f'{pith_name} not string.'
hint = func.__annotations__[pith_name] = Union.__getitem__(hint)
# Else, this hint is *NOT* a PEP-noncompliant tuple union.

# If this object is neither a PEP-noncompliant type hint *NOR* supported
# PEP-compliant type hint, raise an exception.
die_unless_hint(hint=hint, hint_label=hint_label)
# Else, this object is either a PEP-noncompliant type hint *OR* supported
# PEP-compliant type hint.

# Return this hint.
return hint
15 changes: 0 additions & 15 deletions beartype/_decor/_code/_pep/_pephint.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,21 +131,6 @@
#
#To do so, we should probably:
#* In the "beartype._decor._cache.cachehint" submodule:
# * Define a new private "HINT_REPR_TO_HINT" dictionary mapping from hint
# repr() strings to previously cached hints sharing the same repr() strings.
# This dictionary should actually be a trivial dictionary rather than a
# robust LRU cache, because the number of type hints used across a codebase
# is *ALWAYS* miniscule. Moreover, strong references to type hints are
# already stored in "func.__annotations__" dictionaries, so there's no space
# savings in dropping stale references to them in an LRU cache.
# * *ALL* hints except those that are already internally cached (e.g., by the
# "typing" module) should be cached into this dictionary. This obviously
# includes PEP 585-compliant type hints but also *ALL* hints produced by
# resolving deferred PEP 563-based type hint strings. Note that, in the
# latter case, we might want to additionally strip ignorable internal
# whitespace from those strings *IF* those strings contain such whitespace.
# We're pretty sure they don't (because they're programmatically constructed
# by the parser, we think), but we should still investigate this.
# * Any hint that appears in that cache should be *REPLACED* where it appears
# in the "func.__annotations__" dictionary with its cached value. Sweeeeeet.
#* Cache deferred annotations in the "beartype._decor._pep563" submodule. To do
Expand Down
5 changes: 3 additions & 2 deletions beartype/_util/func/utilfuncarg.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
'''

# ....................{ IMPORTS }....................
from collections.abc import Callable
from inspect import CO_VARARGS, CO_VARKEYWORDS

# ....................{ GETTERS }....................
def is_func_arg_variadic(func: 'Callable') -> bool:
# ....................{ TESTERS }....................
def is_func_arg_variadic(func: Callable) -> bool:
'''
``True`` only if the passed pure-Python callable accepts any **variadic
parameters** and thus either variadic positional arguments (e.g.,
Expand Down
58 changes: 58 additions & 0 deletions beartype/_util/func/utilfuncget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2021 Cecil Curry.
# See "LICENSE" for further details.

'''
**Beartype callable getter utilities.**
This private submodule implements utility functions dynamically introspecting
high-level metadata attached to arbitrary callables.
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS }....................
from beartype.roar import _BeartypeUtilCallableException
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
**wrapper** (i.e., higher-level callable typically produced by the
:mod:`functools.wraps` decorator) if this wrapper actually is a wrapper
*or* raise an exception otherwise (i.e., if this wrapper is *not* actually
a wrapper).
Parameters
----------
func : Callable
Wrapper to be unwrapped.
Returns
----------
Callable
Wrappee wrapped by this wrapper.
Raises
----------
_BeartypeUtilCallableException
If this callable is *not* a wrapper.
'''
assert callable(func), f'{repr(func)} not callable.'

# Wrappee wrapped by this wrapper if this wrapper actually is a wrapper
# *OR* "None" otherwise.
wrappee = getattr(func, '__wrapped__', None)

# If this wrapper is *NOT* actually a wrapper, raise an exception.
if wrappee is None:
raise _BeartypeUtilCallableException(
f'Callable {func} not wrapper '
f'(i.e., "__wrapped__" attribute undefined).')
# Else, this wrapper is a wrapper. In this case, this wrappee exists.

# Return this wrappee.
return wrappee

0 comments on commit 76a4457

Please sign in to comment.