|
| 1 | +""" |
| 2 | +Get useful information from live Hy or Python objects. |
| 3 | +
|
| 4 | +This module provides Hy compatibility with CPython's inspect module. |
| 5 | +Within Hy, Hy macros are supported via Hy's `get_macro` macro, |
| 6 | +for example `(inspect.getsource (get-macro get-macro))`. |
| 7 | +The Hy-specific functionality is monkey-patched into `inspect` at run-time. |
| 8 | +
|
| 9 | +The class finder in `findsource` relies on python 3.13 or later. |
| 10 | +""" |
| 11 | + |
| 12 | +import inspect |
| 13 | +import linecache |
| 14 | +import sys |
| 15 | +from contextlib import suppress |
| 16 | + |
| 17 | +from hy.compat import PY3_13 |
| 18 | +from hy.errors import HySyntaxError |
| 19 | +from hy.models import as_model, Expression, Lazy, Object |
| 20 | +from hy.reader import HyReader, read |
| 21 | +from hy.reader.exceptions import LexException, PrematureEndOfInput |
| 22 | + |
| 23 | + |
| 24 | +class HySafeReader(HyReader): |
| 25 | + """A HyReader that skips over non-default reader macros. |
| 26 | +
|
| 27 | + Skip over undefined reader macros so that no code is executed at read time. |
| 28 | + Do this by replacing the tag reader method to return None, as comments do. |
| 29 | + """ |
| 30 | + |
| 31 | + @reader_for(")") |
| 32 | + @reader_for("]") |
| 33 | + @reader_for("}") |
| 34 | + def _skip(self, key): |
| 35 | + return None |
| 36 | + |
| 37 | + @reader_for("#") |
| 38 | + def _safe_tag_dispatch(self, key): |
| 39 | + """Skip over undefined reader macros so that no code is executed at read time.""" |
| 40 | + with suppress(LexException): |
| 41 | + return super().tag_dispatch(key) |
| 42 | + |
| 43 | + |
| 44 | +def isExpression(object): |
| 45 | + """Check if object is a Hy Expression instance.""" |
| 46 | + return isinstance(object, Expression) |
| 47 | + |
| 48 | + |
| 49 | +def isLazy(object): |
| 50 | + """Check if object is a Hy Lazy instance.""" |
| 51 | + return isinstance(object, Lazy) |
| 52 | + |
| 53 | + |
| 54 | +def getfile(object): |
| 55 | + """Work out which source or compiled file an object was defined in.""" |
| 56 | + |
| 57 | + if isLazy(object) or isExpression(object): |
| 58 | + if getattr(object, "filename", None) and object.filename != "<string>": |
| 59 | + return object.filename |
| 60 | + else: |
| 61 | + # python's inspect.getfile raises OSError where there's no __file__ |
| 62 | + raise OSError("source code not available") |
| 63 | + |
| 64 | + try: |
| 65 | + return py_getfile(object) |
| 66 | + except TypeError: |
| 67 | + # python's inspect.getfile raises TypeError for unhandled types |
| 68 | + raise TypeError( |
| 69 | + "module, class, method, function, traceback, frame, " |
| 70 | + "code, Expression or Lazy object was expected, got " |
| 71 | + "{}".format(type(object).__name__) |
| 72 | + ) |
| 73 | + |
| 74 | + |
| 75 | +def findsource(object): |
| 76 | + """Return the entire source file and starting line number for an object. |
| 77 | +
|
| 78 | + First looks for Hy source, otherwise defers to the original |
| 79 | + `inspect.findsource`. The argument may be a module, class, method, |
| 80 | + function, traceback, frame, code, Lazy or Expression object. The source |
| 81 | + code is returned as a list of all the lines in the file and the line number |
| 82 | + indexes a line in that list. An OSError is raised if the source code cannot |
| 83 | + be retrieved. |
| 84 | + """ |
| 85 | + if getfile(object) is None: |
| 86 | + raise OSError("source code not available") |
| 87 | + |
| 88 | + fname = inspect.getsourcefile(object) |
| 89 | + |
| 90 | + if isExpression(object) or isLazy(object): |
| 91 | + if getattr(object, "start_line", None) is None: |
| 92 | + raise OSError("source code not available") |
| 93 | + return (linecache.getlines(fname), object.start_line) |
| 94 | + elif getfile(object).endswith(".hy") and not inspect.isframe(object): |
| 95 | + # We identify other Hy objects from the file extension. |
| 96 | + module = inspect.getmodule(object, fname) |
| 97 | + # List of source code lines. |
| 98 | + lines = ( |
| 99 | + linecache.getlines(fname, module.__dict__) |
| 100 | + if module |
| 101 | + else linecache.getlines(fname) |
| 102 | + ) |
| 103 | + if inspect.ismodule(object): |
| 104 | + return (lines, 0) |
| 105 | + # Some objects already have the information we need. |
| 106 | + elif hasattr(object, "__code__") and hasattr(object.__code__, "co_firstlineno"): |
| 107 | + # This indexing offset is actually correct. |
| 108 | + return (lines, object.__code__.co_firstlineno - 1) |
| 109 | + elif inspect.iscode(object): |
| 110 | + return (lines, object.co_firstlineno - 1) |
| 111 | + elif inspect.isclass(object): |
| 112 | + # _ClassFinder exists and can be used prior to python 3.13, |
| 113 | + # but would require compiling the ast which may be unsafe, |
| 114 | + # so just decline to do it. |
| 115 | + if PY3_13: |
| 116 | + return py_findsource(object) |
| 117 | + else: |
| 118 | + raise OSError("finding Hy class source code requires Python 3.13") |
| 119 | + else: |
| 120 | + raise OSError("source code not available") |
| 121 | + else: |
| 122 | + # Non-Hy object |
| 123 | + return py_findsource(object) |
| 124 | + |
| 125 | + |
| 126 | +def getcomments(object): |
| 127 | + """Get comments immediately preceding an object's source code. |
| 128 | +
|
| 129 | + First checks for Hy source, otherwise defers to the original `inspect.getcomments`. |
| 130 | + Returns None when the source can't be found. |
| 131 | + """ |
| 132 | + try: |
| 133 | + lines, lnum = findsource(object) |
| 134 | + except (OSError, TypeError): |
| 135 | + # Return None when the source can't be found. |
| 136 | + return None |
| 137 | + |
| 138 | + if getfile(object).endswith(".hy"): |
| 139 | + # Roughly follows the logic of inspect.getcomments, but for Hy comments |
| 140 | + if not lines: |
| 141 | + return None |
| 142 | + |
| 143 | + comments = [] |
| 144 | + if inspect.ismodule(object) or isExpression(object) or isLazy(object): |
| 145 | + # Remove shebang. |
| 146 | + start = 1 if lines and lines[0][:2] == "#!" else 0 |
| 147 | + # Remove preceding empty lines and textless comments. |
| 148 | + while start < len(lines) and set(lines[start].strip()) == {";"}: |
| 149 | + start += 1 |
| 150 | + if start < len(lines) and lines[start].lstrip().startswith(";"): |
| 151 | + end = start |
| 152 | + while end < len(lines) and lines[end].lstrip().startswith(";"): |
| 153 | + comments.append(lines[end].expandtabs()) |
| 154 | + end += 1 |
| 155 | + return "".join(comments) |
| 156 | + else: |
| 157 | + return None |
| 158 | + # Look for a comment block preceding the object |
| 159 | + elif lnum > 0 and lnum < len(lines): |
| 160 | + # Look for comments above the object and work up. |
| 161 | + end = lnum - 1 |
| 162 | + while end >= 0 and lines[end].lstrip().startswith(";"): |
| 163 | + comments = [lines[end].expandtabs().lstrip()] + comments |
| 164 | + end -= 1 |
| 165 | + return "".join(comments) if comments else None |
| 166 | + else: |
| 167 | + return None |
| 168 | + |
| 169 | + else: |
| 170 | + # Non-Hy object |
| 171 | + return py_getcomments(object) |
| 172 | + |
| 173 | + |
| 174 | +def getsource(object): |
| 175 | + """Return the text of the source code for an object. |
| 176 | +
|
| 177 | + The argument may be a module, class, method, function, traceback, frame, |
| 178 | + or code object. The source code is returned as a single string. An |
| 179 | + OSError is raised if the source code cannot be retrieved.""" |
| 180 | + return "".join(getsourcelines(object)[0]) |
| 181 | + |
| 182 | + |
| 183 | +def hy_getblock(lines): |
| 184 | + """Extract the lines of code corresponding to the first Hy form from the given list of lines. |
| 185 | +
|
| 186 | + Built-in Hy reader macros are allowed as safe, since they do not execute code. |
| 187 | + User-defined reader macros could execute code, so we use `HySafeReader`. |
| 188 | + """ |
| 189 | + # Read the first form and use its attributes |
| 190 | + try: |
| 191 | + form = read("".join(lines), reader=HySafeReader()) |
| 192 | + except HySyntaxError as e: |
| 193 | + raise e from None |
| 194 | + return lines[: form.end_line] |
| 195 | + |
| 196 | + |
| 197 | +def getsourcelines(object): |
| 198 | + """Return a list of source lines and starting line number for a Hy or python object. |
| 199 | +
|
| 200 | + First checks for Hy source, otherwise defers to the original `inspect.getsourcelines`. |
| 201 | +
|
| 202 | + The argument may be a module, class, method, function, traceback, frame, |
| 203 | + code, Lazy or Expression object. The source code is returned as a list of |
| 204 | + the lines corresponding to the object and the line number indicates where |
| 205 | + in the original source file the first line of code was found. An OSError is |
| 206 | + raised if the source code cannot be retrieved. |
| 207 | + """ |
| 208 | + object = inspect.unwrap(object) |
| 209 | + lines, lnum = findsource(object) |
| 210 | + |
| 211 | + if inspect.istraceback(object): |
| 212 | + object = object.tb_frame |
| 213 | + |
| 214 | + # For module or frame that corresponds to module, return all source lines. |
| 215 | + if inspect.ismodule(object) or ( |
| 216 | + inspect.isframe(object) and object.f_code.co_name == "<module>" |
| 217 | + ): |
| 218 | + return lines, 0 |
| 219 | + |
| 220 | + # Almost everything already works with python's inspect. |
| 221 | + # The inspect.getblock function relies on inspect.BlockFinder which |
| 222 | + # assumes python tokenization. |
| 223 | + # So deal with this as a special case using hy_getblock. |
| 224 | + elif getfile(object).endswith(".hy"): |
| 225 | + return hy_getblock(lines[lnum:]), lnum + 1 |
| 226 | + else: |
| 227 | + # Non-Hy object |
| 228 | + return py_getsourcelines(object) |
| 229 | + |
| 230 | + |
| 231 | +if hasattr(inspect, "_hy_originals"): |
| 232 | + # Retrieve saved versions of `inspect`'s original functions. |
| 233 | + py_findsource, py_getcomments, py_getfile, py_getsource, py_getsourcelines = inspect._hy_originals |
| 234 | +else: |
| 235 | + # Save the originals and then monkey-patch. |
| 236 | + inspect._hy_originals = \ |
| 237 | + py_findsource, py_getcomments, py_getfile, py_getsource, py_getsourcelines = \ |
| 238 | + inspect.findsource, inspect.getcomments, inspect.getfile, inspect.getsource, inspect.getsourcelines |
| 239 | + inspect.findsource, inspect.getcomments, inspect.getfile, inspect.getsource, inspect.getsourcelines = \ |
| 240 | + findsource, getcomments, getfile, getsource, getsourcelines |
0 commit comments