Skip to content

Commit

Permalink
Code object caching.
Browse files Browse the repository at this point in the history
This commit temporarily caches the code object for the currently
decorated callable in preparation for beginning to efficiently
introspect that callable throughout the decoration process. Relatedly,
this also has the beneficial side effect of explicitly raising
human-readable exceptions from the `@beartype` decorator on attempting
to decorate C-based callables (e.g., builtin, third-party C extension
functions), which `@beartype` now explicitly does *not* support, because
C-based callables have *no* code objects and thus *no* efficient means
of introspection. Fortunately, sane code only ever applies `@beartype`
to pure-Python callables. (*Introspective rotoscoped perspective!*)
  • Loading branch information
leycec committed Feb 16, 2021
1 parent 3a25993 commit 28b9e98
Show file tree
Hide file tree
Showing 11 changed files with 111 additions and 61 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/python_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ jobs:
# python -m site

# Note that:
#
# * This command *MUST* be platform-agnostic by running under both:
# * POSIX-compliant platforms (e.g., Linux, macOS).
# * POSIX-noncompliant platforms (e.g., Windows).
Expand All @@ -162,9 +161,9 @@ jobs:
# * Packaging dependencies (e.g., "pip") are upgraded *BEFORE* all
# remaining dependencies (e.g., "tox").
- name: 'Upgrading packager dependencies...'
run: python -m pip install --upgrade pip #setuptools wheel
run: python -m pip --quiet install --upgrade pip #setuptools wheel
- name: 'Installing package dependencies...'
run: python -m pip install --upgrade tox #tox-gh-actions #virtualenv
run: python -m pip --quiet install --upgrade tox #tox-gh-actions #virtualenv

# Note that:
#
Expand Down
3 changes: 2 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@

# ....................{ INCLUDE }....................
# Include all requisite top-level installation-time files.
.readthedocs.yml
include LICENSE
include MANIFEST.in
include README.md
include conftest.py
include pyproject.toml
include pytest.ini
include setup.cfg
include setup.py
include tox.ini

# ....................{ INCLUDE ~ recursive }....................
# Include all requisite project-specific py.test and setuptools subpackages.
Expand Down
22 changes: 11 additions & 11 deletions beartype/_decor/_code/_pep/_pephint.py
Original file line number Diff line number Diff line change
Expand Up @@ -1438,10 +1438,10 @@
#
# # It really is that simple, folks. Maybe. Gods, let it be that simple.
# config = BeartypeConfigGlobal
#* Privatize the existing public "beartype._decor.main" submodule to
# "beartype._decor._decor" or something hopefully less ambiguous.
#* Privatize the existing public "beartype._decor.main" submodule to a new
# "beartype._decor._template" submodule.
#* In that submodule:
# * Rename the existing @beartype decorator to make_func_checker(). That
# * Rename the existing @beartype decorator to beartype_template(). That
# function will now only be called internally rather than externally.
#* Define a new private "beartype._decor._cache.cachedecor" submodule.
#* In that submodule:
Expand Down Expand Up @@ -1469,7 +1469,7 @@
# # "BEARTYPE_PARAMS_TO_DECOR", and return that decorator.
# else:
# # Probably not quite right, but close enough.
# beartype = make_func_checker
# beartype = beartype_template
#
# We need a hashable tuple for lookup purposes. That's *ABSOLUTELY* the
# fastest way, given that we expect keyword arguments. So, we're moving on.
Expand Down Expand Up @@ -1526,7 +1526,7 @@
# param_name, param_value in BEARTYPE_PARAM_NAME_TO_VALUE.items()
# if param_value is not None
# })
# * Dynamically *COPY* the make_func_checker() function into a new
# * Dynamically *COPY* the beartype_template() function into a new
# function specific to that subclass, which means that function is
# actually just a template. We'll never actually the original function
# itself; we just use that function as the basis for dynamically
Expand Down Expand Up @@ -1556,15 +1556,15 @@
# closure=f.__closure__,
# )
# * Monkey-patch the new decorator returned by
# "copy_func(make_func_checker)" with the new subclass: e.g.,
# beartype_decor = copy_func(make_func_checker)
# "copy_func(beartype_template)" with the new subclass: e.g.,
# beartype_decor = copy_func(beartype_template)
# beartype_decor.__beartype_config = BeartypeConfigDecor
# *HMMM.* Minor snag. That doesn't work, but the make_func_checker()
# *HMMM.* Minor snag. That doesn't work, but the beartype_template()
# template won't have access to that "__beartype_config". Instead, we'll
# need to:
# * Augment the signature of the make_func_checker() template to accept
# * Augment the signature of the beartype_template() template to accept
# a new optional "config" parameter default to "None": e.g.,.
# def make_func_checker(
# def beartype_template(
# func: Callable, config: BeartypeConfigGlobal = None) -> Callable:
# * Either refactor the copy_func() function defined above to accept a
# caller-defined "argdefs" parameter *OR* (more reasonably) just
Expand All @@ -1585,7 +1585,7 @@
#Pretty trivial, honestly. We've basically already implemented all of the hard
#stuff above, which is nice.
#
#Note that the make_func_checker() function will now accept an optional
#Note that the beartype_template() function will now accept an optional
#"config" parameter -- which will, of course, *ALWAYS* be non-"None" by the
#logic above. Assert this, of course. We can then trivially expose that
#"config" to lower-level beartype functions by just stuffing it into the
Expand Down
24 changes: 18 additions & 6 deletions beartype/_decor/_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
# ....................{ IMPORTS }....................
import inspect
from beartype.cave import CallableTypes
from beartype.roar import BeartypeDecorWrappeeException
from beartype._util.func.utilfunccodeobj import get_func_codeobj
from beartype._util.text.utiltextlabel import label_callable_decorated

# See the "beartype.__init__" submodule for further commentary.
Expand Down Expand Up @@ -82,6 +84,11 @@ class BeartypeData(object):
func : CallableTypes
**Decorated callable** (i.e., callable currently being decorated by the
:func:`beartype.beartype` decorator).
func_codeobj = CallableCodeObjectType
**Code object** (i.e., instance of the :class:`CodeType` type)
underlying the decorated callable.
func_sig : inspect.Signature
:class:`inspect.Signature` object describing this signature.
Attributes (String)
----------
Expand All @@ -91,11 +98,6 @@ class BeartypeData(object):
clashes with existing attributes of the module defining that function,
this name is obfuscated while still preserving human-readability.
Attributes (Object)
----------
func_sig : inspect.Signature
:class:`inspect.Signature` object describing this signature.
.. _PEP 563:
https://www.python.org/dev/peps/pep-0563
'''
Expand All @@ -107,9 +109,9 @@ class BeartypeData(object):
# write costs by approximately ~10%, which is non-trivial.
__slots__ = (
'func',
'func_codeobj',
'func_sig',
'func_wrapper_name',
'_pep_hint_placeholder_id',
)

# Coerce instances of this class to be unhashable, preventing spurious
Expand Down Expand Up @@ -144,6 +146,7 @@ def __init__(self) -> None:

# Nullify all remaining instance variables.
self.func = None
self.func_codeobj = None
self.func_sig = None
self.func_wrapper_name = None

Expand All @@ -170,6 +173,10 @@ def reinit(self, func: CallableTypes) -> None:
If evaluating a postponed annotation on this callable raises an
exception (e.g., due to that annotation referring to local state no
longer accessible from this deferred evaluation).
BeartypeDecorWrappeeException
If this callable is neither a pure-Python function *nor* method;
equivalently, if this callable is either C-based *or* a class or
object defining the ``__call__()`` dunder method.
.. _PEP 563:
https://www.python.org/dev/peps/pep-0563
Expand All @@ -182,6 +189,11 @@ def reinit(self, func: CallableTypes) -> None:
# Callable currently being decorated.
self.func = func

# Code object underlying this callable if this callable is a
# pure-Python function or method *OR* raise an exception otherwise.
self.func_codeobj = get_func_codeobj(
func=func, exception_cls=BeartypeDecorWrappeeException)

# Machine-readable name of the wrapper function to be generated.
self.func_wrapper_name = f'__beartyped_{func.__name__}'

Expand Down
19 changes: 12 additions & 7 deletions beartype/_decor/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,10 @@
# ....................{ DECORATORS }....................
def beartype(func):
'''
Decorate the passed **callable** (e.g., function, method) to validate both
all annotated parameters passed to this callable *and* the annotated value
returned by this callable if any.
Decorate the passed **pure-Python callable** (e.g., function or method
declared in Python rather than C) to validate both all annotated parameters
passed to this callable *and* the annotated value returned by this callable
if any.
This decorator performs rudimentary type checking based on Python 3.x
function annotations, as officially documented by PEP 484 ("Type Hints").
Expand Down Expand Up @@ -235,16 +236,20 @@ def beartype(func):
fully-qualified classnames).
* **Tuple unions** (i.e., tuples containing one or more classes
and/or forward references).
BeartypeDecorParamNameException
If the name of any parameter declared on this callable is prefixed by
the reserved substring ``__beartype_``.
BeartypeDecorHintPep563Exception
If `PEP 563`_ is active for this callable and evaluating a **postponed
annotation** (i.e., annotation whose value is a string) on this
callable raises an exception (e.g., due to that annotation referring to
local state no longer accessible from this deferred evaluation).
BeartypeDecorParamNameException
If the name of any parameter declared on this callable is prefixed by
the reserved substring ``__beartype_``.
BeartypeDecorWrappeeException
If this callable is either uncallable or a class.
If this callable is either:
* Uncallable.
* A class, which :mod:`beartype` currently fails to support.
* A C-based callable (e.g., builtin, third-party C extension).
.. _PEP 484:
https://www.python.org/dev/peps/pep-0484
Expand Down
24 changes: 18 additions & 6 deletions beartype/_util/func/utilfunccodeobj.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,32 @@
from types import CodeType, FunctionType, MethodType

# ....................{ GETTERS }....................
def get_func_codeobj(func: Callable) -> CodeType:
def get_func_codeobj(
# Mandatory parameters.
func: Callable,

# Optional parameters.
exception_cls: type = _BeartypeUtilCallableException
) -> CodeType:
'''
**Code object** (i.e., instance of the :class:`CodeType` type) underlying
the passed callable if this callable is pure-Python *or* raise an exception
otherwise (e.g., if this callable is C-based or a class or object defining
the ``__call__()`` dunder method).
the passed callable if this callable is a pure-Python function or method
*or* raise an exception otherwise (e.g., if this callable is C-based or a
class or object defining the ``__call__()`` dunder method).
Parameters
----------
func : Callable
Callable to be inspected.
exception_cls : type
Type of exception to be raised if this callable is neither a
pure-Python function nor method. Defaults to
:class:`_BeartypeUtilCallableException`.
Returns
----------
CodeType
Code object underlying this pure-Python callable.
Code object underlying this callable.
Raises
----------
Expand All @@ -48,7 +58,9 @@ def get_func_codeobj(func: Callable) -> CodeType:

# If this callable is *NOT* pure-Python, raise an exception.
if func_codeobj is None:
raise _BeartypeUtilCallableException(
assert isinstance(exception_cls, type), (
f'{repr(exception_cls)} not class.')
raise exception_cls(
f'Callable {repr(func)} code object not found '
f'(e.g., due to being either C-based or a class or object '
f'defining the ``__call__()`` dunder method).'
Expand Down
20 changes: 18 additions & 2 deletions beartype/_util/hint/pep/utilhintpeptest.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,12 @@ def die_if_hint_pep_sign_unsupported(
)

# ....................{ WARNINGS }....................
#FIXME: Resurrect usage 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
#similar replacement for warnings?
def warn_if_hint_pep_sign_deprecated(
# Mandatory parameters.
hint: object,
Expand Down Expand Up @@ -356,10 +362,20 @@ def warn_if_hint_pep_sign_deprecated(

# If this sign is deprecated...
if hint_sign in HINT_PEP_SIGNS_DEPRECATED:
assert isinstance(hint_label, str), f'{repr(hint_label)} not string.'
#FIXME: Uncomment *AFTER* resolving the "FIXME:" above.
#FIXME: Unit test that this string contains *NO* non-human-readable
#placeholder substrings. Note that the existing
#"beartype_test.a00_unit.decor.code.test_codemain" submodule contains
#relevant logic currently disabled for reasons that hopefully no longer
#apply. *Urgh!*

# assert isinstance(hint_label, str), f'{repr(hint_label)} not string.'
#
# # Warning message to be emitted.
# warning_message = f'{hint_label} PEP type hint {repr(hint)} deprecated'

# Warning message to be emitted.
warning_message = f'{hint_label} PEP type hint {repr(hint)} deprecated'
warning_message = f'PEP type hint {repr(hint)} deprecated'

# If this sign uniquely identifies PEP 484-compliant type hints
# originating from origin types (e.g., "typing.List[int]"), this sign
Expand Down
5 changes: 5 additions & 0 deletions beartype_test/a00_unit/a20_api/test_api_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ def test_api_meta() -> None:
assert isinstance(meta.SYNOPSIS, str)
assert isinstance(meta.AUTHORS, str)
assert isinstance(meta.AUTHOR_EMAIL, str)
assert isinstance(meta.COPYRIGHT, str)
assert isinstance(meta.URL_HOMEPAGE, str)
assert isinstance(meta.URL_DOWNLOAD, str)
assert isinstance(meta.URL_ISSUES, str)
assert isinstance(meta.LIBS_RUNTIME_OPTIONAL, tuple)
assert isinstance(meta.LIBS_TESTTIME_MANDATORY, tuple)
assert isinstance(meta.LIBS_TESTTIME_MANDATORY_TOX, tuple)
assert isinstance(meta.LIBS_DOCTIME_MANDATORY, tuple)
assert isinstance(meta.LIBS_DOCTIME_MANDATORY_RTD, tuple)
assert isinstance(meta.LIBS_DEVELOPER_MANDATORY, tuple)

0 comments on commit 28b9e98

Please sign in to comment.