Skip to content

Commit

Permalink
beartype.vale.Is[...] + __call__() x 2.
Browse files Browse the repository at this point in the history
This commit is the last in a commit chain generalizing the
`beartype.vale.Is[...]` validator factory to accept **callable objects**
(i.e., high-level pure-Python objects whose classes define the
`__call__()` dunder method, rendering those objects callable), resolving
feature request #360 kindly submitted by "Computer Graphics and
Visualization" sufferer @sylvorg, who graciously sacrificed his entire
undergraduate GPA for the gradual betterment of @beartype. Your journey
of woe and hardship will *not* be forgotten, @sylvorg! Specifically,
@beartype now accepts class-based beartype validators resembling:

```python
from beartype.door import is_bearable
from beartype.typing import Annotated
from beartype.vale import Is
from functools import partial

class TruthSeeker(object):
    def __call__(self, obj: object) -> bool:
        '''
        Tester method returning :data:`True` only if the passed object
        evaluates to :data:`True` when coerced into a boolean and whose
        first parameter is ignorable.
        '''

        return bool(obj)

# Beartype validator matching only objects that evaluate to "True".
Truthy = Annotated[object, Is[TruthSeeker()]]

assert is_bearable('', Truthy) is False
assert is_bearable('Even lies are true now, huh?', Truthy) is True
```

Is this valuable? I have no idea. Let's pretend I did something useful
tonight so that I can sleep without self-recrimination. (*Painful rain full of insanely manly manes!*)
  • Loading branch information
leycec committed Apr 13, 2024
1 parent 769c7cd commit 75e64bc
Show file tree
Hide file tree
Showing 8 changed files with 413 additions and 249 deletions.
8 changes: 4 additions & 4 deletions beartype/_check/forward/reference/fwdrefmake.py
Expand Up @@ -55,14 +55,14 @@ def make_forwardref_indexable_subtype(
scope_name : Optional[str]
Possibly ignored lexical scope name. Specifically:
* If "hint_name" is absolute (i.e., contains one or more ``.``
* If ``hint_name`` is absolute (i.e., contains one or more ``.``
delimiters), this parameter is silently ignored in favour of the
fully-qualified name of the module prefixing "hint_name".
* If "hint_name" is relative (i.e., contains *no* ``.`` delimiters),
fully-qualified name of the module prefixing ``hint_name``.
* If ``hint_name`` is relative (i.e., contains *no* ``.`` delimiters),
this parameter declares the absolute (i.e., fully-qualified) name of
the lexical scope to which this unresolved type hint is relative.
The fully-qualified name of the module prefixing "hint_name" (if any)
The fully-qualified name of the module prefixing ``hint_name`` (if any)
thus *always* takes precedence over this lexical scope name, which only
provides a fallback to resolve relative forward references. While
unintuitive, this is needed to resolve absolute forward references.
Expand Down
12 changes: 6 additions & 6 deletions beartype/_decor/_decornontype.py
Expand Up @@ -49,9 +49,9 @@
)
from beartype._util.func.utilfuncwrap import (
unwrap_func_once,
unwrap_func_boundmethod,
unwrap_func_classmethod,
unwrap_func_staticmethod,
unwrap_func_boundmethod_once,
unwrap_func_classmethod_once,
unwrap_func_staticmethod_once,
)
from beartype._util.py.utilpyversion import IS_PYTHON_3_8
from contextlib import contextmanager
Expand Down Expand Up @@ -442,7 +442,7 @@ def beartype_descriptor_decorator_builtin(
# Ergo, the name "__func__" of this dunder attribute is disingenuous.
# This descriptor does *NOT* merely decorate functions; this descriptor
# permissively decorates all callable objects.
descriptor_wrappee = unwrap_func_classmethod(descriptor) # type: ignore[arg-type]
descriptor_wrappee = unwrap_func_classmethod_once(descriptor) # type: ignore[arg-type]

# If this wrappee is *NOT* a pure-Python unbound function, this wrappee
# is C-based and/or a type. In either case, avoid type-checking this
Expand Down Expand Up @@ -507,7 +507,7 @@ def beartype_descriptor_decorator_builtin(
# If this descriptor is a static method...
elif descriptor_type is MethodDecoratorStaticType:
# Possibly C-based callable wrappee object decorated by this descriptor.
descriptor_wrappee = unwrap_func_staticmethod(descriptor) # type: ignore[arg-type]
descriptor_wrappee = unwrap_func_staticmethod_once(descriptor) # type: ignore[arg-type]

# Pure-Python unbound function type-checking this static method.
func_checked = beartype_func(descriptor_wrappee, **kwargs) # type: ignore[union-attr]
Expand Down Expand Up @@ -552,7 +552,7 @@ class method defined by the class being instantiated) with dynamically
f'{repr(descriptor)} not builtin bound method descriptor.')

# Possibly C-based callable wrappee object encapsulated by this descriptor.
descriptor_wrappee = unwrap_func_boundmethod(descriptor)
descriptor_wrappee = unwrap_func_boundmethod_once(descriptor)

# Instance object to which this descriptor was bound at instantiation time.
descriptor_self = get_func_boundmethod_self(descriptor)
Expand Down
117 changes: 113 additions & 4 deletions beartype/_util/api/utilapifunctools.py
Expand Up @@ -11,6 +11,7 @@
'''

# ....................{ IMPORTS }....................
from beartype.roar._roarexc import _BeartypeUtilCallableException
from beartype.typing import (
Any,
Tuple,
Expand All @@ -20,11 +21,11 @@
CallableFunctoolsPartialType,
)
from beartype._data.hint.datahintfactory import TypeGuard
from beartype._data.hint.datahinttyping import DictStrToAny
from collections.abc import (
Callable,
# Generator,
from beartype._data.hint.datahinttyping import (
DictStrToAny,
TypeException,
)
from collections.abc import Callable

# ....................{ TESTERS }....................
def is_func_functools_lru_cache(func: Any) -> TypeGuard[Callable]:
Expand Down Expand Up @@ -115,6 +116,114 @@ def get_func_functools_partial_args(
# which this partial was originally partialized.
return (func.args, func.keywords)


def get_func_functools_partial_args_flexible_len(
# Mandatory parameters.
func: CallableFunctoolsPartialType,

# Optional parameters.
is_unwrap: bool = True,
exception_cls: TypeException = _BeartypeUtilCallableException,
exception_prefix: str = '',
) -> int:
'''
Number of **flexible parameters** (i.e., parameters passable as either
positional or keyword arguments but *not* positional-only, keyword-only,
variadic, or other more constrained kinds of parameters) accepted by the
passed **partial** (i.e., pure-Python callable :class:`functools.partial`
object directly wrapping this possibly C-based callable).
Specifically, this getter transparently returns the total number of flexible
parameters accepted by the lower-level callable wrapped by this partial
minus the number of flexible parameters partialized away by this partial.
Parameters
----------
func : CallableFunctoolsPartialType
Partial 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:`.get_func_codeobj` for
further commentary.
exception_cls : type, optional
Type of exception to be raised in the event of a fatal error. Defaults
to :class:`._BeartypeUtilCallableException`.
exception_prefix : str, optional
Human-readable label prefixing the message of any exception raised in
the event of a fatal error. Defaults to the empty string.
Returns
-------
int
Number of flexible parameters accepted by this callable.
Raises
------
exception_cls
If that callable is *not* pure-Python.
'''
assert isinstance(func, CallableFunctoolsPartialType), (
f'{repr(func)} not "function.partial"-wrapped callable.')

# Avoid circular import dependencies.
from beartype._util.func.arg.utilfuncargget import (
get_func_args_flexible_len)

# Pure-Python wrappee callable wrapped by that partial.
wrappee = unwrap_func_functools_partial_once(func)

# Positional and keyword parameters implicitly passed by this partial to
# this wrappee.
partial_args, partial_kwargs = get_func_functools_partial_args(func)

# Number of flexible parameters accepted by this wrappee.
#
# Note that this recursive function call is guaranteed to immediately bottom
# out and thus be safe. Why? Because a partial *CANNOT* wrap itself, because
# a partial has yet to be defined when the functools.partial.__init__()
# method defining that partial is called. Technically, the caller *COULD*
# violate sanity by directly interfering with the "func" instance variable
# of this partial after instantiation. Pragmatically, a malicious edge case
# like that is unlikely in the extreme. You are now reading this comment
# because this edge case just blew up in your face, aren't you!?!? *UGH!*
wrappee_args_flexible_len = get_func_args_flexible_len(
func=wrappee,
is_unwrap=is_unwrap,
exception_cls=exception_cls,
exception_prefix=exception_prefix,
)

# Number of flexible parameters passed by this partial to this wrappee.
partial_args_flexible_len = len(partial_args) + len(partial_kwargs)

# Number of flexible parameters accepted by this wrappee minus the number of
# flexible parameters passed by this partial to this wrappee.
func_args_flexible_len = (
wrappee_args_flexible_len - partial_args_flexible_len)

# If this number is negative, the caller maliciously defined an invalid
# partial passing more flexible parameters than this wrappee accepts. In
# this case, raise an exception.
#
# Note that the "functools.partial" factory erroneously allows callers to
# define invalid partials passing more flexible parameters than their
# wrappees accept. Ergo, validation is required to guarantee sanity.
if func_args_flexible_len < 0:
raise exception_cls(
f'{exception_prefix}{repr(func)} passes '
f'{partial_args_flexible_len} parameter(s) to '
f'{repr(wrappee)} accepting only '
f'{wrappee_args_flexible_len} parameter(s) '
f'(i.e., {partial_args_flexible_len} > '
f'{wrappee_args_flexible_len}).'
)
# Else, this number is non-negative. The caller correctly defined a valid
# partial passing no more flexible parameters than this wrappee accepts.

# Return this number.
return func_args_flexible_len

# ....................{ UNWRAPPERS }....................
def unwrap_func_functools_partial_once(
func: CallableFunctoolsPartialType) -> Callable:
Expand Down

0 comments on commit 75e64bc

Please sign in to comment.