Skip to content

Commit

Permalink
@bearytype + __call__() + __wrapped__ x 3.
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 improves 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. (*Certainly certifiably friable!*)
  • Loading branch information
leycec committed Apr 27, 2024
1 parent 57c0f7b commit 8ce8187
Show file tree
Hide file tree
Showing 25 changed files with 367 additions and 199 deletions.
5 changes: 3 additions & 2 deletions beartype/_check/_checksnip.py
Expand Up @@ -127,8 +127,9 @@
'''
'''
Code snippet suffixing all code type-checking the **root pith** (i.e., value of
the current parameter or return value) against the root type hint annotating
that pith by either raising a fatal exception or emitting a non-fatal warning.
the current parameter or return of a :func:`beartype.beartype`-decorated
callable) against the root type hint annotating that pith by either raising a
fatal exception or emitting a non-fatal warning.
This snippet expects to be formatted with these named interpolations:
Expand Down
35 changes: 18 additions & 17 deletions beartype/_check/checkmagic.py
Expand Up @@ -28,21 +28,32 @@

# ....................{ NAMES ~ parameter }....................
# To avoid colliding with the names of arbitrary caller-defined parameters, the
# beartype-specific parameter names *MUST* be prefixed by "__beartype_".
# beartype-specific hidden parameter names *MUST* be prefixed by "__beartype_".

ARG_NAME_CALL = f'{NAME_PREFIX}call'
'''
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).
'''


ARG_NAME_CONF = f'{NAME_PREFIX}conf'
'''
Name of the **private beartype configuration parameter** (i.e.,
:mod:`beartype`-specific parameter whose default value is the
:mod:`beartype`-specific hidden parameter whose default value is the
:class:`beartype.BeartypeConf` instance configuring each wrapper function
generated by the :func:`beartype.beartype` decorator).
'''


#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 parameter whose default value is the type stack
: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).
Expand All @@ -52,7 +63,7 @@
ARG_NAME_EXCEPTION_PREFIX = f'{NAME_PREFIX}exception_prefix'
'''
Name of the **private exception prefix parameter** (i.e.,
:mod:`beartype`-specific parameter whose default value is the human-readable
:mod:`beartype`-specific hidden parameter whose default value is the human-readable
label prefixing the representation of the currently type-checked object in
exception messages raised when this object violates its type hint, conditionally
passed to wrappers generated by the :func:`beartype.door.die_if_unbearable`
Expand All @@ -61,10 +72,11 @@
'''


#FIXME: Excise us up, please.
ARG_NAME_FUNC = f'{NAME_PREFIX}func'
'''
Name of the **private decorated callable parameter** (i.e.,
:mod:`beartype`-specific parameter whose default value is the decorated
:mod:`beartype`-specific hidden parameter whose default value is the decorated
callable passed to each wrapper function generated by the
:func:`beartype.beartype` decorator).
'''
Expand All @@ -83,7 +95,7 @@
ARG_NAME_GET_VIOLATION = f'{NAME_PREFIX}get_violation'
'''
Name of the **private exception raising parameter** (i.e.,
:mod:`beartype`-specific parameter whose default value is the
:mod:`beartype`-specific hidden parameter whose default value is the
:func:`beartype._check.error.errorget.get_func_pith_violation`
function raising human-readable exceptions on call-time type-checking failures
passed to each wrapper function generated by the :func:`beartype.beartype`
Expand All @@ -100,17 +112,6 @@
'''


#FIXME: Excise us up, pleas. This should no longer be required.
ARG_NAME_TYPISTRY = f'{NAME_PREFIX}typistry'
'''
Name of the **private beartypistry parameter** (i.e., :mod:`beartype`-specific
parameter whose default value is the beartypistry singleton conditionally
passed to every wrapper function generated by the :func:`beartype.beartype`
decorator requiring one or more types or tuples of types cached by this
singleton).
'''


ARG_NAME_WARN = f'{NAME_PREFIX}warn'
'''
Name of the **standard warn function** (i.e., :mod:`beartype`-specific
Expand Down
2 changes: 1 addition & 1 deletion beartype/_check/checkmake.py
Expand Up @@ -34,7 +34,7 @@
get_func_pith_violation,
get_hint_object_violation,
)
from beartype._check.util.checkutilmake import make_func_signature
from beartype._check.signature.sigmake import make_func_signature
from beartype._check._checksnip import (
CODE_CHECKER_SIGNATURE,
CODE_RAISER_FUNC_PITH_CHECK_PREFIX,
Expand Down
2 changes: 1 addition & 1 deletion beartype/_check/code/__init__.py
Expand Up @@ -1808,7 +1808,7 @@
#"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
#existing "BeartypeCall" class: e.g.,
#existing "BeartypeDecorMeta" class: e.g.,
# # Welp, that was trivial.
# func_data.config = config
#
Expand Down
14 changes: 7 additions & 7 deletions beartype/_check/convert/convcoerce.py
Expand Up @@ -63,7 +63,7 @@
from beartype._cave._cavemap import NoneTypeOr
from beartype._data.func.datafuncarg import ARG_NAME_RETURN
from beartype._data.func.datafunc import METHOD_NAMES_DUNDER_BINARY
from beartype._check.checkcall import BeartypeCall
from beartype._check.metadata.metadecor import BeartypeDecorMeta
from beartype._check.forward.fwdmain import resolve_hint
from beartype._util.cache.map.utilmapbig import CacheUnboundedStrong
from beartype._util.hint.utilhinttest import is_hint_uncached
Expand All @@ -75,7 +75,7 @@
def coerce_func_hint_root(
hint: object,
pith_name: Optional[str],
bear_call: BeartypeCall,
decor_meta: BeartypeDecorMeta,
exception_prefix: str,
) -> object:
'''
Expand Down Expand Up @@ -111,7 +111,7 @@ def coerce_func_hint_root(
parameter.
* If this hint annotates the return of some callable, ``"return"``.
* Else, :data:`None`.
bear_call : BeartypeCall
decor_meta : BeartypeDecorMeta
Decorated callable annotated by this hint.
exception_prefix : str
Human-readable label prefixing the representation of this object in the
Expand All @@ -128,8 +128,8 @@ def coerce_func_hint_root(
'''
assert isinstance(pith_name, NoneTypeOr[str]), (
f'{repr(pith_name)} neither string nor "None".')
assert bear_call.__class__ is BeartypeCall, (
f'{repr(bear_call)} not @beartype call.')
assert decor_meta.__class__ is BeartypeDecorMeta, (
f'{repr(decor_meta)} not @beartype call.')
# print(f'Coercing pith "{pith_name}" annotated by type hint {repr(hint)}...')

# ..................{ FORWARD REFERENCE }..................
Expand All @@ -140,7 +140,7 @@ def coerce_func_hint_root(
if isinstance(hint, str):
hint = resolve_hint(
hint=hint,
bear_call=bear_call,
decor_meta=decor_meta,
exception_prefix=exception_prefix,
)
# Else, this hint is *NOT* stringified.
Expand All @@ -153,7 +153,7 @@ def coerce_func_hint_root(
# This hint annotates the return for the decorated callable *AND*...
pith_name == ARG_NAME_RETURN and
# The decorated callable is a binary dunder method (e.g., __eq__())...
bear_call.func_wrapper_name in METHOD_NAMES_DUNDER_BINARY
decor_meta.func_wrapper_name in METHOD_NAMES_DUNDER_BINARY
):
# Expand this hint to accept both this hint *AND* the "NotImplemented"
# singleton as valid returns from this method. Why? Because this
Expand Down
18 changes: 9 additions & 9 deletions beartype/_check/convert/convsanify.py
Expand Up @@ -16,7 +16,7 @@
Any,
Optional,
)
from beartype._check.checkcall import BeartypeCall
from beartype._check.metadata.metadecor import BeartypeDecorMeta
from beartype._check.convert.convcoerce import (
coerce_func_hint_root,
coerce_hint_any,
Expand All @@ -38,7 +38,7 @@ def sanify_hint_root_func(
# Mandatory parameters.
hint: object,
pith_name: str,
bear_call: BeartypeCall,
decor_meta: BeartypeDecorMeta,

# Optional parameters.
exception_prefix: str = EXCEPTION_PLACEHOLDER,
Expand Down Expand Up @@ -93,7 +93,7 @@ class variable or method annotated by this hint *or* :data:`None`).
* If this hint annotates a parameter, the name of that parameter.
* If this hint annotates the return, ``"return"``.
bear_call : BeartypeCall
decor_meta : BeartypeDecorMeta
Decorated callable directly annotated by this hint.
exception_prefix : str, optional
Human-readable label prefixing exception messages raised by this
Expand Down Expand Up @@ -128,10 +128,10 @@ class variable or method annotated by this hint *or* :data:`None`).
# PEP-noncompliant type hint if this hint is coercible *OR* this hint as is
# otherwise. Since the passed hint is *NOT* necessarily PEP-compliant,
# perform this coercion *BEFORE* validating this hint to be PEP-compliant.
hint = bear_call.func_arg_name_to_hint[pith_name] = coerce_func_hint_root(
hint = decor_meta.func_arg_name_to_hint[pith_name] = coerce_func_hint_root(
hint=hint,
pith_name=pith_name,
bear_call=bear_call,
decor_meta=decor_meta,
exception_prefix=exception_prefix,
)

Expand All @@ -148,8 +148,8 @@ class variable or method annotated by this hint *or* :data:`None`).
# thus *NOT* performed by the sanify_hint_root_statement() sanitizer.
if pith_name == ARG_NAME_RETURN:
hint = reduce_hint_pep484585_func_return(
func=bear_call.func_wrappee,
func_arg_name_to_hint=bear_call.func_arg_name_to_hint,
func=decor_meta.func_wrappee,
func_arg_name_to_hint=decor_meta.func_arg_name_to_hint,
exception_prefix=exception_prefix,
)
# Else, this hint annotates a parameter.
Expand Down Expand Up @@ -180,8 +180,8 @@ class variable or method annotated by this hint *or* :data:`None`).
# optimize memoization efficiency and circumvent memoization warnings.
hint = reduce_hint(
hint=hint,
conf=bear_call.conf,
cls_stack=bear_call.cls_stack,
conf=decor_meta.conf,
cls_stack=decor_meta.cls_stack,
pith_name=pith_name,
exception_prefix=exception_prefix,
)
Expand Down
42 changes: 21 additions & 21 deletions beartype/_check/forward/fwdmain.py
Expand Up @@ -21,7 +21,7 @@
)
from beartype.roar._roarexc import _BeartypeUtilCallableScopeNotFoundException
from beartype.typing import Optional
from beartype._check.checkcall import BeartypeCall
from beartype._check.metadata.metadecor import BeartypeDecorMeta
from beartype._check.forward.fwdscope import BeartypeForwardScope
from beartype._data.hint.datahinttyping import TypeException
from beartype._data.kind.datakinddict import DICT_EMPTY
Expand All @@ -40,7 +40,7 @@
def resolve_hint(
# Mandatory parameters.
hint: str,
bear_call: BeartypeCall,
decor_meta: BeartypeDecorMeta,

# Optional parameters.
exception_cls: TypeException = BeartypeDecorHintForwardRefException,
Expand All @@ -63,7 +63,7 @@ def resolve_hint(
----------
hint : str
Stringified type hint to be resolved.
bear_call : BeartypeCall
decor_meta : BeartypeDecorMeta
Decorated callable annotated by this hint.
exception_cls : Type[Exception], optional
Type of exception to be raised in the event of a fatal error. Defaults
Expand Down Expand Up @@ -94,26 +94,26 @@ def resolve_hint(
Python >= 3.10.
'''
assert isinstance(hint, str), f'{repr(hint)} not stringified type hint.'
assert isinstance(bear_call, BeartypeCall), (
f'{repr(bear_call)} not @beartype call.')
assert isinstance(decor_meta, BeartypeDecorMeta), (
f'{repr(decor_meta)} not @beartype call.')
# print(f'Resolving stringified type hint {repr(hint)}...')

# ..................{ LOCALS }..................
# Decorated callable and metadata associated with that callable, localized
# to improve both readability and negligible efficiency when accessed below.
func = bear_call.func_wrappee_wrappee
func = decor_meta.func_wrappee_wrappee

# If the frozen set of the unqualified names of all parent callables
# lexically containing this decorated callable has yet to be decided...
if bear_call.func_wrappee_scope_nested_names is None:
if decor_meta.func_wrappee_scope_nested_names is None:
# Decide this frozen set as either...
bear_call.func_wrappee_scope_nested_names = (
decor_meta.func_wrappee_scope_nested_names = (
# If the decorated callable is nested, the non-empty frozen set of
# the unqualified names of all parent callables lexically containing
# this nested decorated callable (including this nested decorated
# callable itself);
frozenset(func.__qualname__.rsplit(sep='.'))
if bear_call.func_wrappee_is_nested else
if decor_meta.func_wrappee_is_nested else
# Else, the decorated callable is a global function. In this
# case, the empty frozen set.
FROZENSET_EMPTY
Expand Down Expand Up @@ -225,7 +225,7 @@ def resolve_hint(
# * This edge case is both trivial and efficient to support.
#
# tl;dr: Preserve this hint for disambiguity by reducing to a noop.
if hint in bear_call.func_wrappee_scope_nested_names: # type: ignore[operator]
if hint in decor_meta.func_wrappee_scope_nested_names: # type: ignore[operator]
return hint
# Else, this hint is *NOT* the unqualified name of a parent callable or
# class of the decorated callable. In this case, this hint *COULD* require
Expand All @@ -245,9 +245,9 @@ def resolve_hint(
# return 'This is hashable, yo.'

# If the forward scope of the decorated callable has yet to be decided...
if bear_call.func_wrappee_scope_forward is None:
if decor_meta.func_wrappee_scope_forward is None:
# Localize metadata for readability and efficiency. Look. Just do it.
cls_stack = bear_call.cls_stack
cls_stack = decor_meta.cls_stack

# Fully-qualified name of the module declaring the decorated callable,
# which also serves as the name of this module and thus global scope.
Expand All @@ -258,7 +258,7 @@ def resolve_hint(

# If the decorated callable is nested (rather than global) and thus
# *MAY* have a non-empty local nested scope...
if bear_call.func_wrappee_is_nested:
if decor_meta.func_wrappee_is_nested:
# Attempt to...
try:
# Local scope of the decorated callable, localized to improve
Expand Down Expand Up @@ -315,7 +315,7 @@ def resolve_hint(
# additional frames that we could technically ignore. These
# include:
# * The call to the parent
# beartype._check.checkcall.BeartypeCall.reinit() method.
# beartype._check.metadata.metadecor.BeartypeDecorMeta.reinit() method.
# * The call to the parent @beartype.beartype() decorator.
#
# Why? Because the @beartype codebase has been sufficiently
Expand Down Expand Up @@ -469,7 +469,7 @@ def resolve_hint(
# eccentricity would be less efficient and trivial than simply
# initializing this forward scope with all builtin attributes, we prefer
# the current (admittedly sus af) approach. Do not squint at this.
bear_call.func_wrappee_scope_forward = BeartypeForwardScope(
decor_meta.func_wrappee_scope_forward = BeartypeForwardScope(
scope_dict=func_builtins, scope_name=func_module_name)

# Composite this global and local scope into this forward scope (in that
Expand All @@ -478,9 +478,9 @@ def resolve_hint(
# each global and then local attribute of the same name. Since locals
# *ALWAYS* assume precedence over globals *ALWAYS* assume precedence
# over builtins, order of operations is *EXTREMELY* significant here.
bear_call.func_wrappee_scope_forward.update(func_globals)
bear_call.func_wrappee_scope_forward.update(func_locals)
# print(f'Forward scope: {bear_call.func_wrappee_scope_forward}')
decor_meta.func_wrappee_scope_forward.update(func_globals)
decor_meta.func_wrappee_scope_forward.update(func_locals)
# print(f'Forward scope: {decor_meta.func_wrappee_scope_forward}')
# Else, this forward scope has already been decided.
#
# In either case, this forward scope should now all have been decided.
Expand All @@ -489,7 +489,7 @@ def resolve_hint(
# Attempt to resolve this stringified type hint into a non-string type hint
# against both the global and local scopes of the decorated callable.
try:
hint_resolved = eval(hint, bear_call.func_wrappee_scope_forward)
hint_resolved = eval(hint, decor_meta.func_wrappee_scope_forward)
# print(f'Resolved stringified type hint {repr(hint)} to {repr(hint_resolved)}...')
# If doing so failed for *ANY* reason whatsoever...
except Exception as exception:
Expand Down Expand Up @@ -590,10 +590,10 @@ def resolve_hint(
# If the beartype configuration associated with the decorated
# callable enabled debugging, append debug-specific metadata to this
# message.
if bear_call.conf.is_debug:
if decor_meta.conf.is_debug:
exception_message += (
f' Composite global and local scope enclosing this hint:\n\n'
f'{repr(bear_call.func_wrappee_scope_forward)}'
f'{repr(decor_meta.func_wrappee_scope_forward)}'
)
# Else, the beartype configuration associated with the decorated
# callable disabled debugging. In this case, avoid appending
Expand Down
File renamed without changes.

0 comments on commit 8ce8187

Please sign in to comment.