File: utilfuncfile.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 (168 lines) | stat: -rw-r--r-- 7,871 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
#!/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