Skip to content

Commit

Permalink
@bearytype + __call__() + __wrapped__ x 4.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain generalizing the `@beartype`
decorator to support **pseudo-callable wrapper objects** (i.e., objects
defining both the `__call__()` and `__wrapped__` dunder attributes),
en-route to resolving feature request #368 kindly submitted by
@danielward27 (Daniel Ward). Specifically, this commit continues
improving the internal extensibility of @beartype's dynamic code
generator by refactoring:

* The current approach that laboriously passes `n` metadatum as hidden
  parameters to type-checking wrapper functions into...
* A new approach that efficiently and trivially passes a single
  `BeartypeCheckMeta` dataclass instance as a hidden parameter to
  type-checking wrapper functions. Naturally, this instance encapsulates
  those `n` metadatum as instance variables. Naturally, I have no idea
  what I'm talking about. This is why coding and Friday nights is a
  volatile admixture at best.

Frankly, it's best not to think too hard about any of this. (*Externalize the magnetic magma in a diegetic exegesis!*)
  • Loading branch information
leycec committed Apr 30, 2024
1 parent 8ce8187 commit 86c4296
Show file tree
Hide file tree
Showing 29 changed files with 433 additions and 322 deletions.
6 changes: 3 additions & 3 deletions beartype/_cave/_cavefast.py
Expand Up @@ -1452,10 +1452,10 @@ class HintPep695Type(object): pass
'''


#FIXME: Define a new "CallableClassType" by copying the "BoolType" approach
#FIXME: Define a new "ClassCallableType" by copying the "BoolType" approach
#except for the __call__() dunder method instead.
#FIXME: Replace "ClassType" below by "CallableClassType".
#FIXME: Add the "CallableClassType" type to the "CallableTypes" tuple as well.
#FIXME: Replace "ClassType" below by "ClassCallableType".
#FIXME: Add the "ClassCallableType" type to the "CallableTypes" tuple as well.
DecoratorTypes = CallableTypes + (ClassType,)
'''
Tuple of all **decorator types** (i.e., both callable classes *and* the type of
Expand Down
25 changes: 3 additions & 22 deletions beartype/_check/_checksnip.py
Expand Up @@ -14,8 +14,8 @@

# ....................{ IMPORTS }....................
from beartype._check.checkmagic import (
ARG_NAME_CHECK_META,
ARG_NAME_CONF,
ARG_NAME_CLS_STACK,
ARG_NAME_FUNC,
ARG_NAME_EXCEPTION_PREFIX,
ARG_NAME_GET_VIOLATION,
Expand Down Expand Up @@ -119,10 +119,9 @@

CODE_GET_FUNC_PITH_VIOLATION = f''':
{VAR_NAME_VIOLATION} = {ARG_NAME_GET_VIOLATION}(
func={ARG_NAME_FUNC},
conf={ARG_NAME_CONF},
check_meta={ARG_NAME_CHECK_META},
pith_name={{pith_name}},
pith_value={VAR_NAME_PITH_ROOT},{{arg_cls_stack}}{{arg_random_int}}
pith_value={VAR_NAME_PITH_ROOT},{{arg_random_int}}
)
'''
'''
Expand All @@ -133,12 +132,6 @@
This snippet expects to be formatted with these named interpolations:
* ``{arg_cls_stack}``, whose value is either:
* If type-checking for the current type hint requires the type stack,
:data:`.CODE_HINT_ROOT_SUFFIX_CLS_STACK`.
* Else, the empty substring.
* ``{arg_random_int}``, whose value is either:
* If type-checking for the current type hint requires a pseudo-random integer,
Expand All @@ -147,18 +140,6 @@
'''


CODE_GET_VIOLATION_CLS_STACK = f'''
cls_stack={ARG_NAME_CLS_STACK},'''
'''
Code snippet passing the value of the **type stack** (i.e., either a tuple of
the one or more :func:`beartype.beartype`-decorated classes lexically containing
the class variable or method annotated by a type hint type-checked by the larger
code snippet embedding this snippet *or* :data:`None`) required by the current
call to the exception-handling function call embedded in the
:data:`.CODE_HINT_ROOT_SUFFIX` snippet.
'''


CODE_GET_VIOLATION_RANDOM_INT = f'''
random_int={VAR_NAME_RANDOM_INT},'''
'''
Expand Down
24 changes: 6 additions & 18 deletions beartype/_check/checkmagic.py
Expand Up @@ -30,13 +30,13 @@
# To avoid colliding with the names of arbitrary caller-defined parameters, the
# beartype-specific hidden parameter names *MUST* be prefixed by "__beartype_".

ARG_NAME_CALL = f'{NAME_PREFIX}call'
ARG_NAME_CHECK_META = f'{NAME_PREFIX}check_meta'
'''
Name of the **private beartype call metadata** (i.e., :mod:`beartype`-specific
hidden parameter whose default value is the
:class:`beartype._check.metadata.metadecor.BeartypeDecorMeta` dataclass instance encapsulating
*all* metadata for the :func:`beartype.beartype`-decorated callable currently
being type-checked).
Name of the **private beartype type-checking call metadata** (i.e.,
:mod:`beartype`-specific hidden parameter whose default value is the
:class:`beartype._check.metadata.metacheck.BeartypeCheckMeta` dataclass instance
encapsulating *all* metadata required by each call to the wrapper function
type-checking a :func:`beartype.beartype`-decorated callable).
'''


Expand All @@ -49,17 +49,6 @@
'''


#FIXME: Excise us up, please.
ARG_NAME_CLS_STACK = f'{NAME_PREFIX}cls_stack'
'''
Name of the **private decorated type stack parameter** (i.e.,
:mod:`beartype`-specific hidden parameter whose default value is the type stack
conditionally passed to wrappers generated by the :func:`beartype.beartype`
decorator whose type-checking logic requires one or more of the classes
lexically containing the decorated methods wrapped by these wrappers).
'''


ARG_NAME_EXCEPTION_PREFIX = f'{NAME_PREFIX}exception_prefix'
'''
Name of the **private exception prefix parameter** (i.e.,
Expand All @@ -72,7 +61,6 @@
'''


#FIXME: Excise us up, please.
ARG_NAME_FUNC = f'{NAME_PREFIX}func'
'''
Name of the **private decorated callable parameter** (i.e.,
Expand Down
13 changes: 3 additions & 10 deletions beartype/_check/checkmake.py
Expand Up @@ -42,7 +42,6 @@
CODE_TESTER_CHECK_PREFIX,
CODE_GET_FUNC_PITH_VIOLATION,
CODE_GET_HINT_OBJECT_VIOLATION,
CODE_GET_VIOLATION_CLS_STACK,
CODE_GET_VIOLATION_RANDOM_INT,
CODE_RAISE_VIOLATION,
CODE_WARN_VIOLATION,
Expand Down Expand Up @@ -319,19 +318,14 @@ class variable or method annotated by this hint *or* :data:`None`).
''
)

# Code snippet passing the current class stack if needed to type-check this
# type hint, defaulting to *NOT* passing this.
arg_cls_stack = CODE_GET_VIOLATION_CLS_STACK if cls_stack else ''

# Pass hidden parameters to this raiser function exposing the
# get_func_pith_violation() getter called by the
# "CODE_GET_FUNC_PITH_VIOLATION" snippet.
# Expose the get_func_pith_violation() getter called by the
# "CODE_GET_FUNC_PITH_VIOLATION" snippet as a "beartype"-specific hidden
# parameter passed to this wrapper function.
func_scope[ARG_NAME_GET_VIOLATION] = get_func_pith_violation

# Code snippet generating a human-readable violation exception or warning
# when the root pith violates the root type hint.
code_get_violation = CODE_GET_FUNC_PITH_VIOLATION.format(
arg_cls_stack=arg_cls_stack,
arg_random_int=arg_random_int,
pith_name=CODE_PITH_ROOT_NAME_PLACEHOLDER,
)
Expand Down Expand Up @@ -395,7 +389,6 @@ def make_code_raiser_func_pep484_noreturn_check(
# Code snippet generating a human-readable violation exception or warning
# when the root pith violates the root type hint.
code_get_violation = CODE_GET_FUNC_PITH_VIOLATION.format(
arg_cls_stack='',
arg_random_int='',
pith_name=ARG_NAME_RETURN_REPR,
)
Expand Down
7 changes: 0 additions & 7 deletions beartype/_check/code/codemake.py
Expand Up @@ -24,7 +24,6 @@
from beartype.typing import Optional
from beartype._cave._cavefast import TestableTypes
from beartype._check.checkmagic import (
ARG_NAME_CLS_STACK,
ARG_NAME_GETRANDBITS,
VAR_NAME_PITH_ROOT,
)
Expand Down Expand Up @@ -2197,12 +2196,6 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# Else, the breadth-first search above successfully generated code.

# ..................{ CODE ~ scope }..................
# If type-checking for the root pith requires the type stack, pass a hidden
# parameter to this wrapper function exposing this stack.
if cls_stack:
func_wrapper_scope[ARG_NAME_CLS_STACK] = cls_stack
# Else, type-checking for the root pith requires *NO* type stack.

# If type-checking for the root pith requires a pseudo-random integer, pass
# a hidden parameter to this wrapper function exposing the
# random.getrandbits() function required to generate this integer.
Expand Down
50 changes: 27 additions & 23 deletions beartype/_check/error/errorget.py
Expand Up @@ -113,6 +113,7 @@
)
from beartype.typing import Optional
from beartype._check.error._errorcause import ViolationCause
from beartype._check.metadata.metacheck import BeartypeCheckMeta
from beartype._conf.confcls import (
BEARTYPE_CONF_DEFAULT,
BeartypeConf,
Expand All @@ -137,13 +138,13 @@
prefix_pith_value,
)
from beartype._util.text.utiltextrepr import represent_object
from beartype._util.utilobject import SENTINEL
from collections.abc import Callable as CallableABC

# ....................{ GETTERS }....................
def get_func_pith_violation(
# Mandatory parameters.
func: CallableABC,
conf: BeartypeConf,
check_meta: BeartypeCheckMeta,
pith_name: str,
pith_value: object,

Expand All @@ -158,12 +159,10 @@ def get_func_pith_violation(
Parameters
----------
func : CallableTypes
Decorated callable to raise this exception from.
conf : BeartypeConf
**Beartype configuration** (i.e., self-caching dataclass encapsulating
all flags, options, settings, and other metadata configuring the
current decoration of the decorated callable or class).
check_meta : BeartypeCheckMeta
**Beartype type-check call metadata** (i.e., object encapsulating *all*
metadata required by the current call to the wrapper function
type-checking a :func:`beartype.beartype`-decorated callable).
pith_name : str
Either:
Expand Down Expand Up @@ -200,32 +199,37 @@ def get_func_pith_violation(
:func:`.get_hint_object_violation`
Further details.
'''
assert callable(func), f'{repr(func)} uncallable.'
assert isinstance(check_meta, BeartypeCheckMeta), (
f'{repr(check_meta)} not type-checking call metadata.')
assert isinstance(pith_name, str), f'{repr(pith_name)} not string.'

# If this parameter or return value is unannotated, raise an exception.
# Type hint annotating this parameter or return if this parameter or return
# is annotated *OR* the placeholder sentinel otherwise (i.e., if this
# parameter or return is unannotated).
hint = check_meta.func_arg_name_to_hint.get(pith_name, SENTINEL)

# If this parameter or return is unannotated, raise an exception.
#
# Note that this should *NEVER* occur, as the caller guarantees this
# parameter or return to be annotated. However, since malicious callers
# *COULD* deface the "__annotations__" dunder dictionary without our
# knowledge or permission, precautions are warranted.
if pith_name not in func.__annotations__:
raise _BeartypeCallHintPepRaiseException(f'{repr(func)} unannotated.')
# Else, this parameter or return value is annotated.

# Type hint annotating this parameter or return value.
#
# Note that we intentionally avoid calling the __annotations__.get() method
# to obtain this hint. Since "None" is a valid type hint, calling that
# method gains us nothing over the current approach.
hint = func.__annotations__[pith_name]
if hint is SENTINEL:
raise _BeartypeCallHintPepRaiseException(
f'{repr(check_meta.func)} parameter "{pith_name}" unannotated '
f'(or originally annotated but since deleted) in '
f'"__annotations__" dunder dictionary:\n'
f'{repr(check_meta.func_arg_name_to_hint)}'
)
# Else, this parameter or return is annotated.

# Defer to this lower-level violation factory.
return get_hint_object_violation(
obj=pith_value,
cls_stack=check_meta.cls_stack,
conf=check_meta.conf,
func=check_meta.func,
hint=hint,
conf=conf,
func=func,
obj=pith_value,
pith_name=pith_name,
**kwargs
)
Expand Down
75 changes: 71 additions & 4 deletions beartype/_check/metadata/metacheck.py
Expand Up @@ -4,15 +4,17 @@
# See "LICENSE" for further details.

'''
**Beartype dataclass** (i.e., class aggregating *all* metadata for the callable
currently being decorated by the :func:`beartype.beartype` decorator).**
**Beartype type-check call metadata dataclass** (i.e., class aggregating *all*
metadata required by the current call to the wrapper function type-checking a
:func:`beartype.beartype`-decorated callable).
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS }....................
from beartype.typing import TYPE_CHECKING
from beartype._cave._cavemap import NoneTypeOr
from beartype._check.metadata.metadecor import BeartypeDecorMeta
from beartype._conf.confcls import BeartypeConf
from beartype._data.hint.datahinttyping import (
DictStrToAny,
Expand All @@ -25,8 +27,9 @@
class BeartypeCheckMeta(object):
'''
**Beartype type-check call metadata** (i.e., object encapsulating *all*
metadata required by the current call to the wrapper function type-checking
a :func:`beartype.beartype`-decorated callable).
metadata required by each call to the wrapper function type-checking the
callable currently being decorated by the :func:`beartype.beartype`
decorator).
Design
------
Expand Down Expand Up @@ -103,6 +106,14 @@ def __init__(
'''
Initialize this metadata with the passed parameters.
Caveats
-------
**Avoid calling this low-level initializer directly.** Instead,
instantiate instances of this dataclass by calling the
:meth:`make_from_decor_meta` class method -- reducing existing
instances of the parent :class:`.BeartypeDecorMeta` dataclass to
instances of this child dataclass.
Parameters
----------
cls_stack : TypeStack
Expand Down Expand Up @@ -135,3 +146,59 @@ def __init__(
self.conf = conf
self.func = func
self.func_arg_name_to_hint = func_arg_name_to_hint

# ..................{ CLASS METHODS }..................
@classmethod
def make_from_decor_meta(
cls, decor_meta: BeartypeDecorMeta) -> 'BeartypeCheckMeta':
'''
**Beartype type-check call metadata** (i.e., object encapsulating *all*
metadata required by the current call to the wrapper function
type-checking the callable currently being decorated by the
:func:`beartype.beartype` decorator) reduced from the passed **beartype
decorator call metadata** (i.e., object encapsulating *all* metadata for
that callable).
Parameters
----------
decor_meta : BeartypeDecorMeta
Beartype decorator call metadata to be reduced.
'''
assert isinstance(decor_meta, BeartypeDecorMeta)

# Create and return a new instance of this child dataclass reduced from
# the passed parent dataclass.
return BeartypeCheckMeta(
conf=decor_meta.conf,
cls_stack=decor_meta.cls_stack,
func=decor_meta.func_wrappee,
func_arg_name_to_hint=decor_meta.func_arg_name_to_hint,
)


@classmethod
def make_from_decor_meta_kwargs(cls, **kwargs) -> 'BeartypeCheckMeta':
'''
**Beartype type-check call metadata** (i.e., object encapsulating *all*
metadata required by the current call to the wrapper function
type-checking the callable currently being decorated by the
:func:`beartype.beartype` decorator) reduced from the passed **beartype
decorator call metadata keyword parameters** (i.e., keyword parameters
to be passed to the :meth:`BeartypeDecorMeta.reinit` method).
This factory method is a high-level convenience principally intended to
be called from unit tests.
Parameters
----------
All passed keyword parameters are passed as is to the
:meth:`BeartypeDecorMeta.reinit` method.
'''

# Beartype decorator call metadata with which to instantiate a new
# instance of this dataclass.
decor_meta = BeartypeDecorMeta()
decor_meta.reinit(**kwargs)

# Beartype type-checking call metadata reduced from this metadata.
return cls.make_from_decor_meta(decor_meta)
5 changes: 3 additions & 2 deletions beartype/_check/metadata/metadecor.py
Expand Up @@ -4,8 +4,9 @@
# See "LICENSE" for further details.

'''
**Beartype dataclass** (i.e., class aggregating *all* metadata for the callable
currently being decorated by the :func:`beartype.beartype` decorator).**
**Beartype decorator call metadata dataclass** (i.e., class aggregating *all*
metadata for the callable currently being decorated by the
:func:`beartype.beartype` decorator).
This private submodule is *not* intended for importation by downstream callers.
'''
Expand Down

0 comments on commit 86c4296

Please sign in to comment.