Skip to content

Commit

Permalink
@bearytype + __call__() + __wrapped__ x 6.
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 is the last in a
commit subchain generalizing our **argument parser** (i.e., the private
`beartype._util.func.arg.utilfuncargiter.iter_func_args()` generator) to
transparently parse bound method descriptor parameters by omitting the
first mandatory `self` parameter implicitly passed by all bound method
descriptors. Frankly, it's best not to think too hard about any of this.
(*Eruditely lost on Fhloston Paradise!*)
  • Loading branch information
leycec committed May 2, 2024
1 parent 0612105 commit 8fc6e2a
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 114 deletions.
2 changes: 1 addition & 1 deletion beartype/_check/code/codemake.py
Expand Up @@ -600,7 +600,7 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# Localize metadata for both efficiency and f-string purposes.
#
# Note that list unpacking is substantially more efficient than
# manually indexing list items; the former requires only a single Python
# manually indexing list items. The former requires only a single Python
# statement, whereas the latter requires "n" Python statements.
(
hint_curr,
Expand Down
79 changes: 28 additions & 51 deletions beartype/_decor/wrap/_wrapargs.py
Expand Up @@ -42,14 +42,10 @@
reissue_warnings_placeholder,
)
from beartype._util.func.arg.utilfuncargiter import (
ARG_META_INDEX_DEFAULT,
ARG_META_INDEX_KIND,
ARG_META_INDEX_NAME,
ArgKind,
ArgMandatory,
iter_func_args,
)
from beartype._util.func.utilfunctest import is_func_boundmethod
from beartype._util.hint.utilhinttest import (
is_hint_ignorable,
is_hint_needs_cls_stack,
Expand Down Expand Up @@ -133,33 +129,13 @@ def code_check_args(decor_meta: BeartypeDecorMeta) -> str:
#calling the enumerate() builtin here) *AFTER* refactoring @beartype to
#generate callable-specific wrapper signatures.

#FIXME: Heh. Technically, this works -- but it's *SUPER*-awkward and not at
#all the correct location for this logic. Push this low-level hackery down
#into the iter_func_args() iterator, please.

# Arbitrary integer offset to be added to the 0-based index of each
# parameter accepted by the callable currently being decorated. Although
# typically zero, this offset is infrequently set to non-zero integers as
# follows:
# * If that callable is a bound method descriptor encapsulating the unbound
# __call__() dunder method of some type, the
# beartype._decor._decornontype.beartype_pseudofunc() decorator function
# is (probably) currently decorating the pseudo-callable object whose type
# is that type. That decorator wraps that bound method with a dynamically
# generated wrapper function that does *NOT* accept a "self" parameter, as
# a bound method is also guaranteed to *NOT* accept a "self" parameter.
# However, the code object of a bound method descriptor is simply an alias
# of the code object of the corresponding unbound method. Since the latter
# accepts a "self" parameter, so too does the former. Thus, there exists
# an (unfortunate) internal discrepancy in Python between:
# * The code object of a bound method descriptor, which declares that
# callable object to accept a "self" parameter.
# * The real-world calling semantics of a bound method descriptor, which
# by definition accepts *NO* "self" parameter.
ARG_INDEX_OFFSET = 0

if is_func_boundmethod(decor_meta.func_wrappee_wrappee):
ARG_INDEX_OFFSET = -1
# Kind and name of this parameter.
arg_kind: ArgKind = None # type: ignore[assignment]
arg_name: str = None # type: ignore[assignment]

# Default value of this parameter if this parameter is optional *OR* the
# "ArgMandatory" singleton otherwise (i.e., if this parameter is mandatory).
arg_default: object = None

# For the 0-based index of each parameter accepted by that callable and the
# "ParameterMeta" object describing this parameter (in declaration order)...
Expand All @@ -178,26 +154,27 @@ def code_check_args(decor_meta: BeartypeDecorMeta) -> str:
# guaranteed this wrappee to be isomorphically unwrapped.
is_unwrap=False,
)):
#FIXME: Comment us up, please.
arg_index += ARG_INDEX_OFFSET

#FIXME: Comment us up, please. Note that this is a cute (and therefore
#*REALLY* dumb) optimization equivalent to detecting whether
#this is the first mandatory "self" parameter of a bound method
#descriptor.
if arg_index < 0:
continue

#FIXME: [SPEED] Optimize this by assigning all at once via tuple
#unpacking, please. See "codemake" for similar optimizations.
# Kind and name of this parameter.
arg_kind: ArgKind = arg_meta[ARG_META_INDEX_KIND] # type: ignore[assignment]
arg_name: str = arg_meta[ARG_META_INDEX_NAME] # type: ignore[assignment]

# Default value of this parameter if this parameter is optional *OR* the
# "ArgMandatory" singleton otherwise (i.e., if this parameter is
# mandatory).
arg_default: object = arg_meta[ARG_META_INDEX_DEFAULT]
# Localize metadata for both efficiency and f-string purposes.
#
# Note that list unpacking is substantially more efficient than
# manually indexing list items. The former requires only a single Python
# statement, whereas the latter requires "n" Python statements.
(
arg_kind,
arg_name,
arg_default,
) = arg_meta

# #FIXME: [SPEED] Optimize this by assigning all at once via tuple
# #unpacking, please. See "codemake" for similar optimizations.
# # Kind and name of this parameter.
# arg_kind: ArgKind = arg_meta[ARG_META_INDEX_KIND] # type: ignore[assignment]
# arg_name: str = arg_meta[ARG_META_INDEX_NAME] # type: ignore[assignment]
#
# # Default value of this parameter if this parameter is optional *OR* the
# # "ArgMandatory" singleton otherwise (i.e., if this parameter is
# # mandatory).
# arg_default: object = arg_meta[ARG_META_INDEX_DEFAULT]

# Type hint annotating this parameter if any *OR* the sentinel
# placeholder otherwise (i.e., if this parameter is unannotated).
Expand Down
29 changes: 5 additions & 24 deletions beartype/_util/func/arg/utilfuncargget.py
Expand Up @@ -27,35 +27,20 @@
get_func_codeobj_or_none,
get_func_codeobj,
)
from collections.abc import Callable

# ....................{ GETTERS ~ arg }....................
#FIXME: Unit test us up, please.
def get_func_arg_first_name_or_none(
# Mandatory parameters.
func: Callable,

# Optional parameters.
is_unwrap: bool = True,
exception_cls: TypeException = _BeartypeUtilCallableException,
) -> Optional[str]:
def get_func_arg_first_name_or_none(*args, **kwargs) -> Optional[str]:
'''
Name of the first parameter listed in the signature of the passed
pure-Python callable if any *or* :data:`None` otherwise (i.e., if that
callable accepts *no* parameters and is thus parameter-less).
Parameters
----------
func : Codeobjable
Pure-Python callable, frame, or code object to be inspected.
is_unwrap: bool, optional
:data:`True` only if this getter implicitly calls the
:func:`beartype._util.func.utilfuncwrap.unwrap_func_all` function.
Defaults to :data:`True` for safety. See :func:`.iter_func_args` for
further commentary.
exception_cls : type, optional
Type of exception to be raised in the event of a fatal error. Defaults
to :class:`._BeartypeUtilCallableException`.
All arguments are passed as is to the lower-level
:func:`beartype._util.func.arg.utilfuncargiter.iter_func_args` generator
internally deferred to by this higher-level getter.
Returns
-------
Expand All @@ -73,11 +58,7 @@ def get_func_arg_first_name_or_none(
'''

# For metadata describing each parameter accepted by this callable...
for arg_meta in iter_func_args(
func=func,
is_unwrap=is_unwrap,
exception_cls=exception_cls,
):
for arg_meta in iter_func_args(*args, **kwargs):
# Return the name of this parameter.
return arg_meta[ARG_META_INDEX_NAME] # type: ignore[return-value]
# Else, the above "return" statement was *NOT* performed. In this case, this
Expand Down

0 comments on commit 8fc6e2a

Please sign in to comment.