File: utilfunccodeobj.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 (312 lines) | stat: -rw-r--r-- 13,392 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
#!/usr/bin/env python3
# --------------------( LICENSE                            )--------------------
# Copyright (c) 2014-2025 Beartype authors.
# See "LICENSE" for further details.

'''
Project-wide **callable code object utilities** (i.e., callables introspecting
**code objects** (i.e., instances of the :class:`.CallableCodeObjectType` type)
underlying all pure-Python callables).

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

# ....................{ IMPORTS                            }....................
from beartype.roar._roarexc import _BeartypeUtilCallableException
from beartype.typing import (
    Any,
    Optional,
)
from beartype._cave._cavefast import (
    CallableCodeObjectType,
    CallableFrameType,
    FunctionType,
    GeneratorCType,
)
from beartype._data.typing.datatyping import (
    Codeobjable,
    TypeException,
)
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_11

# ....................{ GETTERS                            }....................
def get_func_codeobj(
    # Mandatory parameters.
    func: Codeobjable,

    # Optional parameters.
    is_unwrap: bool = False,
    exception_cls: TypeException = _BeartypeUtilCallableException,
    exception_prefix: str = '',
) -> CallableCodeObjectType:
    '''
    **Code object** (i.e., instance of the :class:`.CallableCodeObjectType`
    type) underlying the passed **codeobjable** (i.e., pure-Python object
    directly associated with a code object) if this object is codeobjable *or*
    raise an exception otherwise (e.g., if this object is *not* codeobjable).

    For convenience, this getter also accepts a code object, in which case that
    code object is simply returned as is.

    Code objects have a docstring under CPython resembling:

    .. code-block:: python

       Code objects provide these attributes:
           co_argcount         number of arguments (not including *, ** args
                               or keyword only arguments)
           co_code             string of raw compiled bytecode
           co_cellvars         tuple of names of cell variables
           co_consts           tuple of constants used in the bytecode
           co_filename         name of file in which this code object was
                               created
           co_firstlineno      number of first line in Python source code
           co_flags            bitmap: 1=optimized | 2=newlocals | 4=*arg |
                               8=**arg | 16=nested | 32=generator | 64=nofree |
                               128=coroutine | 256=iterable_coroutine |
                               512=async_generator
           co_freevars         tuple of names of free variables
           co_posonlyargcount  number of positional only arguments
           co_kwonlyargcount   number of keyword only arguments (not including
                               ** arg)
           co_lnotab           encoded mapping of line numbers to bytecode
                               indices
           co_name             name with which this code object was defined
           co_names            tuple of names of local variables
           co_nlocals          number of local variables
           co_qualname         fully-qualified name with which this code object
                               was defined (Python >= 3.11 only)
           co_stacksize        virtual machine stack space required
           co_varnames         tuple of names of arguments and local variables

    Parameters
    ----------
    func : Codeobjable
        Codeobjable 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 to
        unwrap this possibly higher-level wrapper into a possibly lower-level
        wrappee *before* returning the code object of that wrappee. Note that
        doing so incurs worst-case time complexity :math:`O(n)` for :math:`n`
        the number of lower-level wrappees wrapped by this wrapper. Defaults to
        :data:`False` for efficiency.
    exception_cls : TypeException, 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
    -------
    CallableCodeObjectType
        Code object underlying this codeobjable.

    Raises
    ------
    exception_cls
         If this codeobjable has *no* code object and is thus *not* pure-Python.
    '''

    # Code object underlying this callable if this callable is pure-Python *OR*
    # "None" otherwise.
    func_codeobj = get_func_codeobj_or_none(func=func, is_unwrap=is_unwrap)

    # If this callable is *NOT* pure-Python...
    if func_codeobj is None:
        # Avoid circular import dependencies.
        from beartype._util.func.utilfunctest import die_unless_func_python

        # Raise an exception.
        die_unless_func_python(
            func=func,
            exception_cls=exception_cls,
            exception_prefix=exception_prefix,
        )
    # Else, this callable is pure-Python and this code object exists.

    # Return this code object.
    return func_codeobj  # type: ignore[return-value]


def get_func_codeobj_or_none(
    # Mandatory parameters.
    #
    # Note that the "func" parameter is intentionally annotated as "Any" rather
    # than "Codeobjable", as this tester transparently supports *ALL* objects.
    func: Any,

    # Optional parameters.
    is_unwrap: bool = False,
) -> Optional[CallableCodeObjectType]:
    '''
    **Code object** (i.e., instance of the :class:`.CallableCodeObjectType`
    type) underlying the passed **codeobjable** (i.e., pure-Python object
    directly associated with a code object) if this object is codeobjable *or*
    :data:`None` otherwise (e.g., if this object is *not* codeobjable).

    Specifically, if the passed object is a:

    * Pure-Python function, this getter returns the code object of that
      function (i.e., ``func.__code__``).
    * Pure-Python bound method wrapping a pure-Python unbound function, this
      getter returns the code object of the latter (i.e.,
      ``func.__func__.__code__``).
    * Pure-Python call stack frame, this getter returns the code object of the
      pure-Python callable encapsulated by that frame (i.e., ``func.f_code``).
    * Pure-Python generator, this getter returns the code object of that
      generator (i.e., ``func.gi_code``).
    * Code object, this getter returns that code object as is.
    * Any other object, this getter raises an exception.

    Caveats
    -------
    If ``is_unwrap``, **this callable has worst-case time complexity**
    :math:`O(n)` **for** :math:`n` **the number of lower-level wrappees wrapped
    by this higher-level wrapper.** That parameter should thus be disabled in
    time-critical code paths; instead, the lowest-level wrappee returned by the
    :func:``beartype._util.func.utilfuncwrap.unwrap_func_all` function should be
    temporarily stored and then repeatedly passed.

    Parameters
    ----------
    func : Any
        Codeobjable 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 to
        unwrap this possibly higher-level wrapper into a possibly lower-level
        wrappee *before* returning the code object of that wrappee. Note that
        doing so incurs worst-case time complexity :math:`O(n)` for :math:`n`
        the number of lower-level wrappees wrapped by this wrapper. Defaults to
        :data:`False` for both efficiency and disambiguity.

    Returns
    -------
    Optional[CallableCodeObjectType]
        Either:

        * If this codeobjable is pure-Python, the code object underlying this
          codeobjable.
        * Else, :data:`None`.

    See Also
    --------
    :func:`.get_func_codeobj`
        Further details.
    '''
    assert isinstance(is_unwrap, bool), f'{is_unwrap} not boolean.'

    # Avoid circular import dependencies.
    from beartype._util.func.utilfunctest import is_func_boundmethod
    from beartype._util.func.utilfuncwrap import (
        unwrap_func_all,
        unwrap_func_boundmethod_once,
    )

    # For efficiency, tests are intentionally ordered in decreasing likelihood
    # of a successful match. An equivalent algorithm could also technically be
    # written as a chain of "getattr(func, '__code__', None)" calls, but doing
    # so would both be less efficient *AND* render this getter less robust. Why?
    # Because the getattr() builtin internally calls the __getattr__() and
    # __getattribute__() dunder methods (either of which could raise arbitrary
    # exceptions) and is thus considerably less safe.

    # If this object is already a code object, return this object as is.
    if isinstance(func, CallableCodeObjectType):
        return func
    # Else, this object is *NOT* already a code object.
    #
    # If this callable is a bound method, reduce this callable to the unbound
    # function underlying this bound method.
    elif is_func_boundmethod(func):
        func = unwrap_func_boundmethod_once(func)
    # Else, this callable is *NOT* a pure-Python bound method.

    # Code object to be returned, defaulting to "None".
    func_codeobj = None

    # If this object is a pure-Python function...
    #
    # Note that:
    # * This test is intentionally a new "if" conditional rather than an
    #   extension of the prior "elif" conditional. Doing so trivially unwraps
    #   the pure-Python function encapsulated by a bound method descriptor.
    # * This test intentionally leverages the standard "FunctionType"
    #   class rather than our equivalent "beartype.cave.FunctionType" class to
    #   avoid circular import issues.
    if isinstance(func, FunctionType):
        # Return the code object of either:
        # * If unwrapping this function, the lowest-level wrappee wrapped by
        #   this function.
        # * Else, this function as is.
        func_codeobj = (unwrap_func_all(func) if is_unwrap else func).__code__  # type: ignore[attr-defined]
    # Else, this object is *NOT* a pure-Python function.
    #
    # If this object is a pure-Python generator, return this generator's code
    # object.
    elif isinstance(func, GeneratorCType):
        func_codeobj = func.gi_code
    # Else, this object is *NOT* a pure-Python generator.
    #
    # If this object is a call stack frame, return this frame's code object.
    elif isinstance(func, CallableFrameType):
        #FIXME: *SUS AF.* This is likely to behave as expected *ONLY* for frames
        #encapsulating pure-Python callables. For frames encapsulating C-based
        #callables, this is likely to fail with an "AttributeError" exception.
        #That said, we have *NO* idea how to test this short of defining our own
        #C-based callable accepting a pure-Python callable as a callback
        #parameter and calling that callback. Are there even C-based callables
        #like that in the wild?
        func_codeobj = func.f_code
    # Else, this object is *NOT* a call stack frame. Since none of the above
    # tests matched, this object *MUST* be a C-based callable. Return "None"!

    # Return this code object.
    return func_codeobj

# ....................{ GETTERS                            }....................
#FIXME: Unit test us up, please.
def get_func_codeobj_basename(func: Codeobjable, **kwargs) -> str:
    '''
    Unqualified basename (contextually depending on the version of the active
    Python interpreter) of the passed **codeobjable** (i.e., pure-Python object
    directly associated with a code object) if this object is codeobjable *or*
    raise an exception otherwise (e.g., if this object is *not* codeobjable).

    Specifically, this getter returns:

    * If the active Python interpreter targets Python >= 3.11, the value of the
      the ``co_qualname`` attribute on this code object.
    * Else, the value of the ``co_name`` attribute on this code object.

    Parameters
    ----------
    func : Codeobjable
        Codeobjable to be inspected.

    All remaining keyword parameters are passed as is to the
    :func:`.get_func_codeobj` getter.

    Raises
    ------
    exception_cls
         If this codeobjable has *no* code object and is thus *not* pure-Python.
    '''

    # Code object underlying this codeobjable if pure-Python *OR* raise an
    # exception otherwise (i.e., if this codeobjable is C-based).
    func_codeobj = get_func_codeobj(func, **kwargs)

    # Return either...
    return (
        # If the active Python interpreter targets Python >= 3.11 and thus
        # defines the "co_qualname" attribute on code objects, that attribute;
        func_codeobj.co_qualname  # type: ignore[attr-defined]
        if IS_PYTHON_AT_LEAST_3_11 else
        # Else, the active Python interpreter targets Python < 3.11 and thus
        # does *NOT* defines the "co_qualname" attribute on code objects. In
        # this case, the "co_name" attribute instead.
        func_codeobj.co_name
    )