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
)
|