Skip to content

Commit 2e4a80f

Browse files
authored
Merge pull request #2678 from atisharma/inspect
Add `hy.inspect` module, extending `inspect`.
2 parents 8a32e57 + 7db27e3 commit 2e4a80f

File tree

11 files changed

+495
-5
lines changed

11 files changed

+495
-5
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,4 @@
110110
* Joseph LaFreniere <[email protected]>
111111
* Daniel Tan <[email protected]>
112112
* Zhan Tang <[email protected]>
113+
* Ati Sharma <[email protected]>

LICENSE

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
1919
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
2020
DEALINGS IN THE SOFTWARE.
2121

22-
Portions of hy/contrib/pprint.hy copyright 2020 Python Software Foundation,
23-
licensed under the Python Software Foundation License Version 2.
22+
Portions of `tests/resources/hy_inspect/fodder_1.hy` are copyright 2001
23+
Python Software Foundation, licensed under the Python Software Foundation
24+
License Version 2.

NEWS.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ Supports Python 3.x – Python 3.y
1010
New Features
1111
------------------------------
1212
* `setv` now supports chained assignment with `:chain`.
13+
* Several functions in the standard `inspect` module have been
14+
monkey-patched to work better with Hy code: `findsource`,
15+
`getcomments`, `getfile`, `getsource`, `getsourcelines`.
16+
17+
* As a result, the `ll` command in `pdb` should now show a more
18+
useful result in more cases.
1319

1420
1.1.0 ("Business Hugs", released 2025-05-08)
1521
======================================================================

hy/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ def _initialize_env_var(env_var, default_val):
1212
return bool(os.environ.get(env_var, default_val))
1313

1414

15-
import hy.importer # NOQA
16-
15+
# Import for side-effects.
16+
import hy.importer, hy.hy_inspect
1717
hy.importer._inject_builtins()
18-
# we import for side-effects.
18+
1919

2020

2121
class I:

hy/hy_inspect.py

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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

hy/reader/reader.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ def wrapper(f):
3636
def __new__(cls, name, bases, namespace):
3737
del namespace["reader_for"]
3838
default_table = {}
39+
for base in bases:
40+
if hasattr(base, "DEFAULT_TABLE"):
41+
default_table |= base.DEFAULT_TABLE
3942
for method in namespace.values():
4043
if callable(method) and hasattr(method, "_readers"):
4144
default_table.update(method._readers)

0 commit comments

Comments
 (0)