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
|
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2025 Beartype authors.
# See "LICENSE" for further details.
'''
Project-wide **callable origin** (i.e., uncompiled source from which a compiled
callable originated) utilities.
This private submodule implements supplementary callable-specific utility
functions required by various :mod:`beartype` facilities, including callables
generated by the :func:`beartype.beartype` decorator.
This private submodule is *not* intended for importation by downstream callers.
'''
# ....................{ TODO }....................
#FIXME: *FILE UPSTREAM CPYTHON ISSUES.* Unfortunately, this submodule exposed a
#number of significant issues in the CPython stdlib -- all concerning parsing
#of lambda functions. These include:
#
#1. The inspect.getsourcelines() function raises unexpected
# "tokenize.TokenError" exceptions when passed lambda functions preceded by
# one or more triple-quoted strings: e.g.,
# >>> import inspect
# >>> built_to_fail = (
# ... ('''Uh-oh.
# ... ''', lambda obj: 'Oh, Gods above and/or below!'
# ... )
# ... )
# >>> inspect.getsourcelines(built_to_fail[1])}
# tokenize.TokenError: ('EOF in multi-line string', (323, 8))
#FIXME: Contribute get_func_code_or_none() back to this StackOverflow question
#as a new answer, as this is highly non-trivial, frankly:
# https://stackoverflow.com/questions/59498679/how-can-i-get-exactly-the-code-of-a-lambda-function-in-python/64421174#64421174
# ....................{ IMPORTS }....................
from beartype.typing import Optional
from beartype._data.typing.datatyping import Codeobjable
from beartype._util.func.utilfunccodeobj import get_func_codeobj_or_none
from linecache import cache as linecache_cache
# ....................{ TESTERS }....................
def is_func_file(func: Codeobjable) -> bool:
'''
:data:`True` only if the passed callable is defined **on-disk** (e.g., by a
script or module whose pure-Python source code is accessible to the active
Python interpreter as a file on the local filesystem).
Equivalently, this tester returns :data:`False` if that callable is
dynamically defined in-memory (e.g., by a prior call to the :func:`exec` or
:func:`eval` builtins).
Parameters
----------
func : Codeobjable
Codeobjable to be inspected.
Returns
-------
bool
:data:`True` only if the passed callable is defined on-disk.
'''
# One-liners for abstruse abstraction.
return get_func_filename_or_none(func) is not None
# ....................{ GETTERS }....................
def get_func_filename_or_none(func: Codeobjable, **kwargs) -> Optional[str]:
'''
Absolute filename of the file on the local filesystem containing the
pure-Python source code for the script or module defining the passed
callable if that callable is defined on-disk *or* :data:`None` otherwise
(i.e., if that callable is dynamically defined in-memory by a prior call to
the :func:`exec` or :func:`eval` builtins).
Parameters
----------
func : Codeobjable
Codeobjable to be inspected.
All remaining keyword parameters are passed as is to the
:func:`beartype._util.func.utilfunccodeobj.get_func_codeobj` getter.
Returns
-------
Optional[str]
Either:
* If that callable was physically declared by a file, the absolute
filename of that file.
* If that callable was dynamically declared in-memory, :data:`None`.
'''
# Code object underlying the passed callable if that callable is pure-Python
# *OR* "None" otherwise (i.e., if that callable is C-based).
#
# Note that we intentionally do *NOT* test whether this callable is
# explicitly pure-Python or C-based: e.g.,
# # If this callable is implemented in C, this callable has no code
# # object with which to inspect the filename declaring this callable.
# # In this case, defer to a C-specific placeholder string.
# if isinstance(func, CallableCTypes):
# func_origin_label = '<C-based>'
# # Else, this callable is implemented in Python. In this case...
# else:
# # If this callable is a bound method wrapping an unbound function,
# # unwrap this method into the function it wraps. Why? Because only
# # the latter provides the code object for this callable.
# if isinstance(func, MethodBoundInstanceOrClassType):
# func = func.__func__
#
# # Defer to the absolute filename of the Python file declaring this
# # callable, dynamically retrieved from this callable's code object.
# func_origin_label = func.__code__.co_filename
#
# Why? Because PyPy. The logic above succeeds for CPython but fails for
# PyPy, because *ALL CALLABLES ARE C-BASED IN PYPY.* Adopting the above
# approach would unconditionally return the C-specific placeholder string
# for all callables -- including those originally declared as pure-Python in
# a Python module. So it goes.
func_codeobj = get_func_codeobj_or_none(func, **kwargs)
# If the passed callable has *NO* code object and is thus *NOT* pure-Python,
# that callable was *NOT* defined by a pure-Python source code file. In this
# case, return "None".
if not func_codeobj:
return None
# Else, that callable is pure-Python.
# Absolute filename of the pure-Python source code file defining that
# callable if this code object offers that metadata *OR* "None" otherwise.
#
# Note that we intentionally do *NOT* assume all code objects to offer this
# metadata (e.g., by unconditionally returning "func_codeobj.co_filename").
# Why? Because PyPy yet again. For inexplicable reasons, PyPy provides
# *ALL* C-based builtins (e.g., len()) with code objects failing to provide
# this metadata. Yes, this is awful. Yes, this is the Python ecosystem.
func_filename = getattr(func_codeobj, 'co_filename', None)
# If either this code object does not provide this filename *OR*...
if not func_filename or (
# This filename is a "<"- and ">"-bracketed placeholder string, this
# filename is a placeholder signifying this callable to be dynamically
# declared in-memory rather than by an on-disk module. Examples include:
# * "<string>", signifying a callable dynamically declared in-memory.
# * "<@beartype(...) at 0x...}>', signifying a callable dynamically
# declared in-memory by the
# beartype._util.func.utilfuncmake.make_func() function, possibly
# cached with the standard "linecache" module.
func_filename[ 0] == '<' and
func_filename[-1] == '>' and
# This in-memory callable's source code was *NOT* cached with the
# "linecache" module and has thus effectively been destroyed.
func_filename not in linecache_cache
# Then return "None", as this filename is useless for almost all purposes.
):
return None
# Else, this filename is either:
# * That of an on-disk module, which is good.
# * That of an in-memory callable whose source code was cached with the
# "linecache" module. Although less good, this filename *CAN* technically
# be used to recover this code by querying the "linecache" module.
# Return this filename as is, regardless of whether this file exists.
# Callers are responsible for performing further validation if desired.
# print(f'func_filename: {func_filename}')
return func_filename
|