-
Notifications
You must be signed in to change notification settings - Fork 375
Add hy.inspect module, extending inspect.
#2678
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
As I understand, you're providing some functions that can be used in place of
Isn't that the main point of the linked issue, though? Without this, are we really addressing the issue? If I understand your xfail marks correctly, you believe that methods should actually inherit the docstrings of superclass methods. This doesn't happen in Python, so it would be weird for it to happen in Hy. Rather, as I understand it, Using Python's |
Yes, I suppose, but as per a previous discussion I had understood that you were against monkey-patching
The issue shows The comment you mention refers to the missing test def test_getsource_on_code_object(self):
self.assertSourceEqual(mod.eggs.__code__, 12, 18)which would test for
I see. I haven't reimplemented
I've rewritten the two tests that used the |
You mean the discussion in #2578? I was complaining about undecidable syntax and surprise code execution, not monkey-patching.
The goal was to support "code that implicitly uses |
|
Curiously, it seems you can. For example, the file test_pdb.hy: ;; test pdb and hy.inspect
(defn foo []
(setv x 1)
(for [y (range 2)]
(print y))
(breakpoint)
(print x))
(defclass A []
(setv x 0)
(defn [property] y [] self.x)
(defn incr [self]
(setv self.x (+ 1 self.x))
(breakpoint)))
(setv a (A))
(foo)
(a.incr)
(breakpoint)when run as I'm not familiar enough with Hy's import priority to know why it prioritises hy.inspect, but it seems to, and this avoids monkey-patching. |
|
Hmm, I get a different result: Is it possible that your environment was a bit screwy when you ran that? In Python in general, a danger of giving a module the same name as a standard module, like |
|
It was a clean venv, so that's odd. I'll go the monkey-patch route then. |
|
To be clear, you should rename the module to something else unless you're confident you can keep it from shadowing the standard |
|
Quite so. Thanks for your help with this. I have taken the following approach, based on your feedback:
This 'works for me'. |
|
Just to add - I don't know if |
|
Thanks, I did a deeper dive into this and accumulated a long list of nitpicks, but one last big-picture issue we should resolve before worrying about that stuff is the use of |
|
If it helps, => (import hy.reader.hy_reader [HyReader])
=> (import hy [read])
=> (defreader m 'x)
=>
=> (read "#_(ignored) (symbol #[[string]] #m)" :reader (HyReader :use-current-readers False))
Traceback (most recent call last):
File "stdin-646bae656e43034675a545ddad37cffa4d69bd84", line 1, in <module>
(read "#_(ignored) (symbol #[[string]] #m)" :reader (HyReader :use-current-readers False))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<string>", line 1
#_(ignored) (symbol #[[string]] #m)
^^
hy.reader.exceptions.LexException: reader macro '#m' is not defined
=>
=> (read "#_(ignored) (symbol #[[string]] #m)" :reader (HyReader :use-current-readers True))
'(symbol #[[string]] x)This does raise the question of whether |
|
@Kodiologist correct, nothing in the standard reader table allows for arbitrary code execution @atisharma I think we might need to use something different than the standard HyReader to parse "raw" hy forms, since (defreader m
'(print "hi"))
(defn foo []
#m
(breakpoint))
(foo)the half-highlight of Potentially it might be easier to make a separate reader class? Since we don't actually need most of the machinery of HyReader generating hy models, we just need text extraction. |
Isn't that kinda what it should do? We don't want to execute the macro, but we also can't read without executing the macro. Refusing to proceed seems like the right choice. |
|
Well, blowing up on reader macros now kind of makes this even worse than before, since instead of at least printing a single line with I'd argue we shouldn't be using HyReader in the first place, and we should be using something different/simpler to extract raw text segments. Either that or have a like "raw mode" option in HyReader to skim over reader macros instead of raising a LexException. |
|
I see your point. Is that something we could do separately? This is already a big PR and I'm asking a lot of the author. |
|
I feel like trying to skip reader-macro calls could be a losing game, because after the macro name, there is no guarantee that what follows is anything resembling legal ordinary Hy syntax. All kinds of parse errors could result. But the proof is in the pudding. |
I think it's fine for these inspect functions to assume "loosely well-formed" Hy; if someone is using custom readers that would mess with their own editor's syntax highlighting, then I think it's reasonable to assume they would understand why the debugger isn't displaying their function properly. |
|
Okay. So we could add something like an option to |
|
I agree that this approach is reasonable because it mirrors of python's inspect. Although, I do feel like we need to be explicit about what types of reader macro can be expected to work and which should not, and document the choice. I have tried subclassing HyReader and overriding the I also would like your views on how to handle the error when you hit a reader macro that doesn't work when you skip over the reader macro identifier. At the moment |
|
Here's a long list of (mostly) small comments. I've made you do a lot for this PR already, so let me know if any of this is too troublesome and I'll take care of it myself.
|
|
@atisharma good catch on the inheritance bug! Turns out it's an easy fix; if you patch the following: --- a/hy/reader/reader.py
+++ b/hy/reader/reader.py
@@ -36,6 +36,9 @@ class ReaderMeta(type):
def __new__(cls, name, bases, namespace):
del namespace["reader_for"]
default_table = {}
+ for base in bases:
+ if hasattr(base, "DEFAULT_TABLE"):
+ default_table |= base.DEFAULT_TABLE
for method in namespace.values():
if callable(method) and hasattr(method, "_readers"):
default_table.update(method._readers)then you can vastly simplify HySafeReader: class HySafeReader(HyReader):
@reader_for("#")
def _safe_tag_dispatch(self, key):
with suppress(LexException):
return super().tag_dispatch(key)Adding on a few more comments (which I'm also happy to jump in and do): |
|
Great. I can do most of that over the next day or two. There are a few items I'll leave for you both. |
|
I've had a go at most of these. I have left a few that are subjective, or quicker or easier for you to do it, or where I'm not quite sure what you mean.
The reader after the fix is indeed much cleaner.
|
|
Turns out that the only meanginful test against fodder_2 is redundant with a test against fodder_1, so I can remove fodder_2 entirely, shaving a good 400 lines from this PR. |
I agree with the second sentence, but I can't find the indication of public domain. In https://www.python.org/ftp/python/3.14.2/Python-3.14.2.tar.xz , looking at |
|
My mistake; the comment about public domain is the actual inspect module: |
|
Okay, I'll take care of it. |
|
I've finished the aforementioned tasks. There's a wrinkle, which is that better testing seems to have revealed some bugs. @atisharma, could you take a look at these test failures? I'll clean up the commit structure later. |
|
For the test that looks here in fodder-1, 55 ; Test that getsource works on a line that includes
56 ; a closing parenthesis with the opening paren being in another line
57 #(
58 ) (setv after_closing (fn [] 1))it's tripping up here because it starts reading at line 58 column 0, which causes a LexError because of the unpaired closing paren. This is quite a good test; I'd normally not encounter it because parinfer would move the closing paren up, but it is of course valid hy. For this particular case (a code object) we could use PEP 626's co_lines method to get the bytecode offset for the start. But I'm not sure that would help in a similar situation with a class definition, for example, where there is no code object. The alternative is to further abuse the reader to just carry on instead of raising LexError, by overwriting the reader's INVALID method. Which is less pretty but probably robust. Unless you prefer otherwise, I'll abuse the reader to do this. |
|
For the ll test, as you point out, it seems like for 3.9, it fails to print the source. For 3.10, 3.11, 3.12, ll actually works, but pdb thinks it's on l4, so needs a special case: For 3.13, 3.14, works as-is. I'm not sure whether it's worth pursuing 3.9, since it was EOL earlier this year. |
|
Also I notice hy.compat does some monkey patching of getdoc, which falls back to inspect.getcomments. I don't know whether that logic ought to change. |
I suspect that's the way to do it, because there are probably other ways to provoke an error. For example, you could use a reader macro, which, by design, our new safe reader won't parse correctly.
Is the misplaced arrow not a bug?
Yeah, you can just add a test skip for Python 3.9 if you want. We're dropping support for that as soon as it becomes more than slightly inconvenient.
It might no longer be necessary. |
|
Cool. I pushed the reader change. Probably the l4 arrow is a bug, but I don't think that it's in hy_inspect. I think the line number is generated from the bytecode object. The implementation was overhauled in 3.13. |
|
Did you commit the wrong thing? All I see is a whitespace change. |
|
I did, sorry. Parinfer automatically messes with the whitespace. |
|
Thanks.
You're probably right. I'll have to conditionally xfail the test for now. |
|
@scauligi I'd like to clean up the commits and then merge this. Is that okay by you? |
|
Ah, while you were doing that I captured the pre-3.13 output and rewrote the test to handle that case, so it passes. Do you want me to push it? |
|
No, that output appears to be wrong; I think it would be counterproductive to test for it. |
lemme give it one more quick once-over today, but otherwise I'm ideologically unopposed 👍 |
|
|
||
| First looks for Hy source, otherwise defers to the original | ||
| `inspect.findsource`. The argument may be a module, class, method, | ||
| function, traceback, frame, code, Lazy or Expression object. The source |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lazy or Expression object
doesn't seem to be true; passing in the output of hy.read or hy.read-all throws AttributeError: 'NoneType' object has no attribute 'endswith' (from inspect.getsourcefile below).
in any case, is there a reason we need this to work on models? I'd expect inspect functions to only be called on (compiled) objects (@atisharma ?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed a fix. It's maybe useful when you want to read a file and you've set the filename.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I slept on this and realised you're right - there's not a great need to support Lazy or Expression. Removing that path doesn't break any tests. Still, it's there and is not much code, and can always be removed if it causes problems later.
scauligi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aight that's everything I had, @Kodiologist I assume you're handling the fixup/squash and rebase?
thanks for the contribution @atisharma !
|
I will, yes. |
|
That you both very much for your help. I am pleased to be able to give something back. It should help some with the tooling situation. |
Some tests are drawn from CPython. Co-authored-by: Kodi Arfer <[email protected]> Co-authored-by: Sunjay Cauligi <[email protected]>
|
All done, and it only took 3 PhDs to do it. Thanks, guys. |
Introduces
hy.inspectto support Hy-specific introspection, extendingpython's
inspectmodule.It does this by shadowing the functions getfile, findsource,
getsourcelines, getcomments, getsource for Hy constructs. It adds
isExpression and isLazy, and hy_getblock (the equivalent to getblock).
Works for macros via get-macro.
Has test coverage equivalent to python's inspect for the new features.
Tests include xfails where Hy's class docstring inheritance differs from
python's Tests are skipped where class source is unavailable (pre-Python
3.13).
Fixes #1696.