File: utilfuncwrap.py

package info (click to toggle)
python-beartype 0.22.9-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 9,504 kB
  • sloc: python: 85,502; sh: 328; makefile: 30; javascript: 18
file content (488 lines) | stat: -rw-r--r-- 19,057 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
#!/usr/bin/env python3
# --------------------( LICENSE                            )--------------------
# Copyright (c) 2014-2025 Beartype authors.
# See "LICENSE" for further details.

'''
Project-wide **callable wrapper** (i.e., higher-level callable, typically
implemented as a decorator, wrapping a lower-level callable) utilities.

This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS                            }....................
from beartype.roar._roarexc import _BeartypeUtilCallableWrapperException
from beartype.typing import (
    Optional,
    Union,
)
from beartype._cave._cavefast import MethodBoundInstanceOrClassType
from beartype._data.typing.datatyping import TypeException
from collections.abc import Callable

# ....................{ UNWRAPPERS ~ once                  }....................
#FIXME: Unit test us up, please.
def unwrap_func_once(func: Callable) -> Callable:
    '''
    Immediate **wrappee** (i.e., callable wrapped by the passed wrapper
    callable) of the passed higher-level **wrapper** (i.e., callable wrapping
    the wrappee callable to be returned) if the passed callable is a wrapper
    *or* that callable as is otherwise (i.e., if that callable is *not* a
    wrapper).

    Specifically, this getter undoes the work performed by any of the following:

    * A single use of the :func:`functools.wrap` decorator on the wrappee
      callable to be returned.
    * A single call to the :func:`functools.update_wrapper` function on the
      wrappee callable to be returned.

    Parameters
    ----------
    func : Callable
        Wrapper callable to be unwrapped.

    Returns
    -------
    Callable
        The immediate wrappee callable wrapped by the passed wrapper callable.

    Raises
    ------
    _BeartypeUtilCallableWrapperException
        If the passed callable is *not* a wrapper.
    '''

    # Immediate wrappee callable wrapped by the passed wrapper callable if any
    # *OR* "None" otherwise (i.e., if that callable is *NOT* a wrapper).
    func_wrappee = getattr(func, '__wrapped__', None)

    # If that callable is *NOT* a wrapper, raise an exception.
    if func_wrappee is None:
        raise _BeartypeUtilCallableWrapperException(
            f'Callable {repr(func)} not wrapper '
            f'(i.e., has no "__wrapped__" dunder attribute '
            f'defined by @functools.wrap or functools.update_wrapper()).'
        )
    # Else, that callable is a wrapper.

    # Return this immediate wrappee callable.
    return func_wrappee

# ....................{ UNWRAPPERS ~ once : descriptor     }....................
#FIXME: Unit test us up, please.
def unwrap_func_boundmethod_once(
    # Mandatory parameters.
    func: MethodBoundInstanceOrClassType,

    # Optional parameters.
    exception_cls: TypeException = _BeartypeUtilCallableWrapperException,
    exception_prefix: str = '',
) -> Callable:
    '''
    Pure-Python unbound function wrapped by the passed **C-based bound instance
    method descriptor** (i.e., callable implicitly instantiated and assigned on
    the instantiation of an object whose class declares an instance function
    (whose first parameter is typically named ``self``) as an instance variable
    of that object such that that callable unconditionally passes that object as
    the value of that first parameter on all calls to that callable).

    Parameters
    ----------
    func : MethodBoundInstanceOrClassType
        Bound method descriptor to be inspected.
    exception_cls : TypeException, optional
        Type of exception to be raised in the event of a fatal error. Defaults
        to :exc:`._BeartypeUtilCallableWrapperException`.
    exception_prefix : str, optional
        Human-readable label prefixing the representation of this object in the
        exception message. Defaults to the empty string.

    Returns
    -------
    Callable
        Pure-Python unbound function wrapped by this bound method descriptor.

    Raises
    ------
    exception_cls
         If the passed object is *not* a bound method descriptor.

    See Also
    --------
    :func:`beartype._util.func.utilfunctest.is_func_boundmethod`
        Further details.
    '''

    # Avoid circular import dependencies.
    from beartype._util.func.utilfunctest import die_unless_func_boundmethod

    # If this object is *NOT* a class method descriptor, raise an exception.
    die_unless_func_boundmethod(
        func=func,
        exception_cls=exception_cls,
        exception_prefix=exception_prefix,
    )
    # Else, this object is a class method descriptor.

    # Return the pure-Python function wrapped by this descriptor. Just do it!
    return func.__func__


def unwrap_func_class_or_static_method_once(
    # Mandatory parameters.
    func: Union[classmethod, staticmethod],

    # Optional parameters.
    exception_cls: TypeException = _BeartypeUtilCallableWrapperException,
    exception_prefix: str = '',
) -> Callable:
    '''
    Pure-Python unbound function wrapped by the passed **C-based unbound class
    or static method descriptor** (i.e., method decorated by either the builtin
    :class:`classmethod` or :class:`staticmethod` decorators, yielding a
    non-callable instance of that decorator class implemented in low-level C and
    accessible via the low-level :attr:`object.__dict__` dictionary rather than
    as class or instance attributes).

    Parameters
    ----------
    func : Union[classmethod, staticmethod]
        Class or static method descriptor to be inspected.
    exception_cls : TypeException, optional
        Type of exception to be raised in the event of a fatal error. Defaults
        to :exc:`._BeartypeUtilCallableWrapperException`.
    exception_prefix : str, optional
        Human-readable label prefixing the representation of this object in the
        exception message. Defaults to the empty string.

    Returns
    -------
    Callable
        Pure-Python unbound function wrapped by this method descriptor.

    Raises
    ------
    exception_cls
         If the passed object is *not* a class or static method descriptor.

    See Also
    --------
    :func:`beartype._util.func.utilfunctest.is_func_classmethod`
    :func:`beartype._util.func.utilfunctest.is_func_staticmethod`
        Further details.
    '''

    # Avoid circular import dependencies.
    from beartype._util.func.utilfunctest import (
        die_unless_func_class_or_static_method)

    # If this object is neither a class *NOR* static method descriptor, raise an
    # exception.
    die_unless_func_class_or_static_method(
        func=func,
        exception_cls=exception_cls,
        exception_prefix=exception_prefix,
    )
    # Else, this object is either a class or static method descriptor.

    # Return the pure-Python function wrapped by this descriptor. Just do it!
    return func.__func__


#FIXME: Currently unused, but extensively tested. *shrug*
def unwrap_func_classmethod_once(
    # Mandatory parameters.
    func: classmethod,

    # Optional parameters.
    exception_cls: TypeException = _BeartypeUtilCallableWrapperException,
    exception_prefix: str = '',
) -> Callable:
    '''
    Pure-Python unbound function wrapped by the passed **C-based unbound class
    method descriptor** (i.e., method decorated by the builtin
    :class:`classmethod` decorator, yielding a non-callable instance of that
    :class:`classmethod` decorator class implemented in low-level C and
    accessible via the low-level :attr:`object.__dict__` dictionary rather than
    as class or instance attributes).

    Parameters
    ----------
    func : classmethod
        Class method descriptor to be inspected.
    exception_cls : TypeException, optional
        Type of exception to be raised in the event of a fatal error. Defaults
        to :exc:`._BeartypeUtilCallableWrapperException`.
    exception_prefix : str, optional
        Human-readable label prefixing the representation of this object in the
        exception message. Defaults to the empty string.

    Returns
    -------
    Callable
        Pure-Python unbound function wrapped by this class method descriptor.

    Raises
    ------
    exception_cls
         If the passed object is *not* a class method descriptor.

    See Also
    --------
    :func:`beartype._util.func.utilfunctest.is_func_classmethod`
        Further details.
    '''

    # Avoid circular import dependencies.
    from beartype._util.func.utilfunctest import die_unless_func_classmethod

    # If this object is *NOT* a class method descriptor, raise an exception.
    die_unless_func_classmethod(
        func=func,
        exception_cls=exception_cls,
        exception_prefix=exception_prefix,
    )
    # Else, this object is a class method descriptor.

    # Return the pure-Python function wrapped by this descriptor. Just do it!
    return func.__func__


#FIXME: Currently unused, but extensively tested. *shrug*
def unwrap_func_staticmethod_once(
    # Mandatory parameters.
    func: staticmethod,

    # Optional parameters.
    exception_cls: TypeException = _BeartypeUtilCallableWrapperException,
    exception_prefix: str = '',
) -> Callable:
    '''
    Pure-Python unbound function wrapped by the passed **C-based unbound static
    method descriptor** (i.e., method decorated by the builtin
    :class:`staticmethod` decorator, yielding a non-callable instance of that
    :class:`staticmethod` decorator class implemented in low-level C and
    accessible via the low-level :attr:`object.__dict__` dictionary rather than
    as class or instance attributes).

    Parameters
    ----------
    func : staticmethod
        Static method descriptor to be inspected.
    exception_cls : TypeException, optional
        Type of exception to be raised in the event of a fatal error. Defaults
        to :exc:`._BeartypeUtilCallableWrapperException`.
    exception_prefix : str, optional
        Human-readable label prefixing the representation of this object in the
        exception message. Defaults to the empty string.

    Returns
    -------
    Callable
        Pure-Python unbound function wrapped by this static method descriptor.

    Raises
    ------
    exception_cls
         If the passed object is *not* a static method descriptor.

    See Also
    --------
    :func:`beartype._util.func.utilfunctest.is_func_staticmethod`
        Further details.
    '''

    # Avoid circular import dependencies.
    from beartype._util.func.utilfunctest import die_unless_func_staticmethod

    # If this object is *NOT* a static method descriptor, raise an exception.
    die_unless_func_staticmethod(
        func=func,
        exception_cls=exception_cls,
        exception_prefix=exception_prefix,
    )
    # Else, this object is a static method descriptor.

    # Return the pure-Python function wrapped by this descriptor. Just do it!
    return func.__func__

# ....................{ UNWRAPPERS ~ all                   }....................
def unwrap_func_all(func: Callable) -> Callable:
    '''
    Lowest-level **wrappee** (i.e., callable wrapped by the passed wrapper
    callable) of the passed higher-level **wrapper** (i.e., callable wrapping
    the wrappee callable to be returned) if the passed callable is a wrapper
    *or* that callable as is otherwise (i.e., if that callable is *not* a
    wrapper).

    Specifically, this getter iteratively undoes the work performed by:

    * One or more consecutive uses of the :func:`functools.wrap` decorator on
      the wrappee callable to be returned.
    * One or more consecutive calls to the :func:`functools.update_wrapper`
      function on the wrappee callable to be returned.

    Parameters
    ----------
    func : Callable
        Wrapper callable to be unwrapped.

    Returns
    -------
    Callable
        Either:

        * If the passed callable is a wrapper, the lowest-level wrappee
          callable wrapped by that wrapper.
        * Else, the passed callable as is.
    '''

    #FIXME: Not even this suffices to avoid a circular import, sadly. *sigh*
    # Avoid circular import dependencies.
    # from beartype._util.func.utilfunctest import is_func_wrapper

    # While this callable still wraps another callable, unwrap one layer of
    # wrapping by reducing this wrapper to its next wrappee.
    while hasattr(func, '__wrapped__'):
    # while is_func_wrapper(func):
        func = func.__wrapped__  # type: ignore[attr-defined]

    # Return this wrappee, which is now guaranteed to *NOT* be a wrapper.
    return func


def unwrap_func_all_isomorphic(
    # Mandatory parameters.
    func: Callable,

    # Optional parameters.
    wrapper: Optional[Callable] = None,
) -> Callable:
    '''
    Lowest-level **non-isomorphic wrappee** (i.e., callable wrapped by the
    passed wrapper callable) of the passed higher-level **isomorphic wrapper**
    (i.e., closure wrapping the wrappee callable to be returned by accepting
    both a variadic positional and keyword argument and thus preserving both the
    positions and types of all parameters originally passed to that wrappee) if
    the passed callable is an isomorphic wrapper *or* that callable as is
    otherwise (i.e., if that callable is *not* an isomorphic wrapper).

    Specifically, this getter iteratively undoes the work performed by:

    * One or more consecutive decorations of the :func:`functools.wrap`
      decorator on the wrappee callable to be returned.
    * One or more consecutive calls to the :func:`functools.update_wrapper`
      function on the wrappee callable to be returned.

    Parameters
    ----------
    func : Callable
        Wrapper callable to be inspected for isomorphism. If ``wrapper`` is
        :data:`None` (as is the common case), this callable is also unwrapped.
    wrapper : Optional[Callable]
        Wrapper callable to be unwrapped in the event that the callable to be
        inspected for isomorphism differs from the callable to be unwrapped.
        Typically, these two callables are the same. Edge cases in which these
        two callables differ include:

        * When ``wrapper`` is a **pseudo-callable** (i.e., otherwise uncallable
          object whose type renders that object callable by defining the
          ``__call__()`` dunder method) *and* ``func`` is that ``__call__()``
          dunder method. If that pseudo-callable wraps a lower-level callable,
          then that pseudo-callable (rather than ``__call__()`` dunder method)
          defines the ``__wrapped__`` instance variable providing that callable.

        Defaults to :data:`None`, in which case this parameter *actually*
        defaults to ``func``.

    Returns
    -------
    Callable
        Either:

        * If the passed callable is an isomorphic wrapper, the lowest-level
          non-isomorphic wrappee callable wrapped by that wrapper.
        * Else, the passed callable as is.
    '''

    # Avoid circular import dependencies.
    from beartype._util.func.utilfunctest import (
        # is_func_boundmethod,
        is_func_codeobjable,
        is_func_wrapper_isomorphic,
    )

    # If the caller failed to explicitly pass a callable to be unwrapped,
    # default the callable to be unwrapped to the passed callable.
    if wrapper is None:
        wrapper = func
    # Else, the caller explicitly passed a callable to be unwrapped. In this
    # case, preserve that callable as is.

    # While...
    while True:
        # This wrappee callable remains a higher-level wrapper callable
        # isomorphically wrapping a lower-level wrappee callable, undo one layer
        # of wrapping by reducing the former to the latter.
        if is_func_wrapper_isomorphic(func=func, wrapper=wrapper):
            func_wrapped = unwrap_func_once(wrapper)
            # print(f'Unwrapped isomorphic {repr(func)} wrapper {repr(wrapper)} to {repr(func_wrapped)}.')
        # Else, this wrappee callable is no longer a higher-level wrapper
        # callable isomorphically wrapping a lower-level wrappee callable.

        #FIXME: Unneeded at the moment, but preserved for posterity. *shrug*
        # # If this wrappee callable remains a higher-level bound method
        # # descriptor, this descriptor transparently proxies and thus (by
        # # definition) isomorphically wraps a lower-level unbound method. In this
        # # case, undo one layer of wrapping by reducing the former to the latter.
        # elif is_func_boundmethod(func):
        #     func_wrapped = unwrap_func_boundmethod_once(func)
        #     # print(f'Unwrapped bound method descriptor {repr(func)} to {repr(func_wrapped)}.')

        # Else, this wrappee callable is no longer a higher-level bound method
        # descriptor either. Since this wrappee callable no longer wraps
        # anything, halt iteration.
        else:
            break
        # print(f'Unwrapped isomorphic {repr(func)} wrapper {repr(wrapper)} to {repr(func_wrapped)}.')

        # If the lower-level object wrapped by this higher-level isomorphic
        # wrapper is *NOT* a pure-Python callable, this object is something
        # uselessly pathological like a class or C-based callable. Silently
        # ignore this useless object by halting iteration. Doing so preserves
        # this useful higher-level isomorphic wrapper as is.
        #
        # Note that this insane edge case arises due to the standard
        # @functools.wraps() decorator, which passively accepts possibly C-based
        # classes by wrapping those classes with pure-Python functions: e.g.,
        #     from beartype import beartype
        #     from functools import wraps
        #     from typing import Any
        #
        #     @beartype
        #     @wraps(list)
        #     def wrapper(*args: Any, **kwargs: Any):
        #         return list(*args, **kwargs)
        #
        # In the above example, the higher-level isomorphic wrapper wrapper()
        # wraps the lower-level C-based class "list".
        #
        # Unwrapping this wrapper to this class would induce insanity throughout
        # the codebase, which sanely expects wrappers to be callables rather
        # than classes. Clearly, classes have *NO* signatures. Technically, a
        # pure-Python class may define __new__() and/or __init__() dunder
        # methods that could be considered to be the signatures of those
        # classes. Nonetheless, C-based classes like "list" have *NO* such
        # analogues. The *ONLY* sane approach here is to pretend that we never
        # saw this pathological edge case.
        if not is_func_codeobjable(func_wrapped):
            break
        # Else, this lower-level callable is pure-Python.

        # Reduce this higher-level wrapper to this lower-level wrappee.
        func = wrapper = func_wrapped

    # Return this wrappee, which is now guaranteed to *NOT* be an isomorphic
    # wrapper but might very well still be a wrapper, which is fine.
    return func