Skip to content

completion for remote pdb is not ideal #133351

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

Closed
gaogaotiantian opened this issue May 3, 2025 · 12 comments
Closed

completion for remote pdb is not ideal #133351

gaogaotiantian opened this issue May 3, 2025 · 12 comments
Labels
stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error

Comments

@gaogaotiantian
Copy link
Member

gaogaotiantian commented May 3, 2025

Bug report

Bug description:

Due to various reasons, remote pdb client does not sync with the server entirely. For example, for multi-line inputs, it will read the full content on the client then send it to the server.

However, this introduced the issue for completion - the server doesn't know it. When user requests a completion for the multi-line code, server pdb doesn't know what completion should be provided (more specifically, whether pdb commands should be provided).

The simplest way to deal with it is to add an extra field "mode" in completion protocol - unless we sync the client and server perfectly, we will need it because the server knows nothing about the client.

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Linked PRs

@gaogaotiantian gaogaotiantian added the type-bug An unexpected behavior, bug, or error label May 3, 2025
@gaogaotiantian
Copy link
Member Author

I put it under the bug label for now, but maybe it's a feature. It's very close to beta freeze now, I think we should at least update the protocol so we can try to keep the protocol version and solve the issue for 3.14. I don't think the code is too complicated.

@pablogsal @godlygeek

@godlygeek
Copy link
Contributor

godlygeek commented May 3, 2025

Hm. I see two alternative fixes that wouldn't require changing the protocol schema:

  1. If we're doing a multi-line input and asking for completion for a line other than the first, prefix the line we send to the server with ! so that the server knows it must be a Python command. We'll need to adjust the offsets by 1, but otherwise I think things would Just Work.

  2. Maybe we should send the whole buffered multi-line input to the server, instead of just the current line? PDB doesn't currently use that information in its completions, but maybe it should, in a perfect world. As an example, imagine someone writes:

(PDB) def foo():
...       some_long_variable_name = []
...       some<Tab>

PDB can't currently complete the local variable name, but wouldn't it be nice if it could? It could heuristically do that by parsing the written-but-not-yet-executed text looking for assignment statements and adding the names on the LHS to its list of possible completions. But for that to work, the server would need to get the full context of what's buffered, not just the last line of it. (Admittedly though this is a slippery slope, you'll never get something perfect without being able to actually execute the code and introspect its objects, so it's reasonable to decide that you don't want to implement this feature at all because of the fact that there would always be requests to do more - like recognize that it's a list and complete list methods for it, or the like.)

@gaogaotiantian
Copy link
Member Author

I don't like sending with ! because that's the explicit "pdb command" indicator for pdb. The server needs to know that this is something that should not include pdb command completions.

PyREPL does not even handle the local variable names defined in the previous lines for now. I think that is rather complicated because that requires constantly compile code that might not be compilable.

Even if you passed the whole multi-line string, you probably will need some logic to figure out whether that's a Python command or not (checking \n? not sure if it's reliable). The existing protocol is mimicking what readline actually does - if we change the text, we probably will simulate something that readline is not supposed to deal with (does readline work with multiline context?).

If you think it's easier to deal with this case with a full context, we can try that.

@godlygeek
Copy link
Contributor

I don't like sending with ! because that's the explicit "pdb command" indicator for pdb. The server needs to know that this is something that should not include pdb command completions.

Huh. It seems like this is something that regular PDB gets wrong, actually. If I do:

(Pdb) !apple = "apple"
(Pdb) !abba = "abba"
(Pdb) !a<Tab>

I would expect it to complete the two variable names, since the ! means that it's completing in the context of a Python statement to be exec'd, but it doesn't show any completions.

If we fix that, so that

(Pdb) !anything<Tab>

and

(Pdb) if True:
...       anything<Tab>

show the same completions, then my quick fix will work.

@godlygeek
Copy link
Contributor

There's a cmd.Cmd bug here, that's why it's not working.

cpython/Lib/cmd.py

Lines 275 to 280 in 2bc8365

cmd, args, foo = self.parseline(line)
if cmd == '':
compfunc = self.completedefault
else:
try:
compfunc = getattr(self, 'complete_' + cmd)

parseline can return with cmd set to None, and if it does the if cmd == '': block doesn't run, so it instead tries to do compfunc = getattr(self, 'complete_' + cmd), but since cmd is None that fails with a TypeError instead of the AttributeError that it's expecting to handle, and so it never falls back to completedefault like it was meant to.

After fixing that, ! a<Tab> (with a space after the !) does complete the 'a' names that it should, and we should be able to get the behavior that we want by having remote PDB send a "! " prefix for the line, and incrementing both the indexes by 2.

@gaogaotiantian
Copy link
Member Author

Ah right my brain went somewhere else. ! should forces default behavior.

Do you want to make a PR to fix that? I can do it too. Either change line 276 to cmd == '' or cmd is None or just flip the logic and do if cmd:. We do need regression test and a news entry.

@godlygeek
Copy link
Contributor

After #133364 is applied I think this patch makes completion do what you want:

diff --git a/Lib/pdb.py b/Lib/pdb.py
index 343cf4404d7..eaea762e2db 100644
--- a/Lib/pdb.py
+++ b/Lib/pdb.py
@@ -2933,6 +2933,7 @@ def __init__(self, pid, sockfile, interrupt_script):
         self.completion_matches = []
         self.state = "dumb"
         self.write_failed = False
+        self.multiline_block = False

     def _ensure_valid_message(self, msg):
         # Ensure the message conforms to our protocol.
@@ -2979,6 +2980,7 @@ def _send(self, **kwargs):
             self.write_failed = True

     def read_command(self, prompt):
+        self.multiline_block = False
         reply = input(prompt)

         if self.state == "dumb":
@@ -3003,6 +3005,7 @@ def read_command(self, prompt):
             return prefix + reply

         # Otherwise, valid first line of a multi-line statement
+        self.multiline_block = True
         continue_prompt = "...".ljust(len(prompt))
         while codeop.compile_command(reply, "<stdin>", "single") is None:
             reply += "\n" + input(continue_prompt)
@@ -3105,9 +3108,13 @@ def complete(self, text, state):

             origline = readline.get_line_buffer()
             line = origline.lstrip()
-            stripped = len(origline) - len(line)
-            begidx = readline.get_begidx() - stripped
-            endidx = readline.get_endidx() - stripped
+            if self.multiline_block:
+                # We're completing a line contained in a multi-line block.
+                # Force the remote to treat it as a Python expression.
+                line = "! "
+            offset = len(origline) - len(line)
+            begidx = readline.get_begidx() - offset
+            endidx = readline.get_endidx() - offset

             msg = {
                 "complete": {

Can you try that out and see if you spot any other problems?

@gaogaotiantian
Copy link
Member Author

Is this correct? Shouldn't it be line = "! " + line?

@godlygeek
Copy link
Contributor

Hah! Yes it should. Believe it or not it actually works even with that bug! 😆

@godlygeek
Copy link
Contributor

It does also work with that bug fixed, so we should definitely do what you suggest, heh

@gaogaotiantian
Copy link
Member Author

I guess text was the only thing used in completedefault in pdb.Pdb so the others do not matter, but yeah we should fix it or it will confuse the people in the future.

@picnixz picnixz added the stdlib Python modules in the Lib dir label May 4, 2025
diegorusso added a commit to diegorusso/cpython that referenced this issue May 4, 2025
* origin/main: (111 commits)
  pythongh-91048: Add filename and line number to external inspection routines (pythonGH-133385)
  pythongh-131178: Add tests for `ast` command-line interface (python#133329)
  Regenerate pcbuild.sln in Visual Studio 2022 (python#133394)
  pythongh-133042: disable HACL* HMAC on Emscripten (python#133064)
  pythongh-133351: Fix remote PDB's multi-line block tab completion (python#133387)
  pythongh-109700: Improve stress tests for interpreter creation (pythonGH-109946)
  pythongh-81793: Skip tests for os.link() to symlink on Android (pythonGH-133388)
  pythongh-126835: Rename `ast_opt.c` to `ast_preprocess.c` and related stuff after moving const folding to the peephole optimizier (python#131830)
  pythongh-91048: Relax test_async_global_awaited_by to fix flakyness (python#133368)
  pythongh-132457: make staticmethod and classmethod generic (python#132460)
  pythongh-132805: annotationlib: Fix handling of non-constant values in FORWARDREF (python#132812)
  pythongh-132426: Add get_annotate_from_class_namespace replacing get_annotate_function (python#132490)
  pythongh-81793: Always call linkat() from os.link(), if available (pythonGH-132517)
  pythongh-122559: Synchronize C and Python implementation of the io module about pickling (pythonGH-122628)
  pythongh-69605: Add PyREPL import autocomplete feature to 'What's New' (python#133358)
  bpo-44172: Keep reference to original window in curses subwindow objects (pythonGH-26226)
  pythonGH-133231: Changes to executor management to support proposed `sys._jit` module (pythonGH-133287)
  pythongh-133363: Fix Cmd completion for lines beginning with `! ` (python#133364)
  pythongh-132983: Introduce `_zstd` bindings module (pythonGH-133027)
  pythonGH-91048: Add utils for printing the call stack for asyncio tasks (python#133284)
  ...
@godlygeek
Copy link
Contributor

I think this can be closed now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

3 participants