From 061eeb8245fd4e1995c133638edba0909801df91 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Mon, 16 Oct 2023 11:31:46 -0700 Subject: [PATCH 1/5] Make pdb completion work for alias and convenience vars --- Lib/pdb.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 930cb91b8696a3..c2b36c50e16e71 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -232,7 +232,7 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, try: import readline # remove some common file name delimiters - readline.set_completer_delims(' \t\n`@#$%^&*()=+[{]}\\|;:\'",<>?') + readline.set_completer_delims(' \t\n`@#%^&*()=+[{]}\\|;:\'",<>?') except ImportError: pass self.allow_kbdint = False @@ -670,6 +670,19 @@ def set_convenience_variable(self, frame, name, value): # Generic completion functions. Individual complete_foo methods can be # assigned below to one of these functions. + def completenames(self, text, line, begidx, endidx): + # Overwrite completenames() of cmd so for the command completion, + # if no current command matches, check for expressions as well + dotext = 'do_' + text + commands = [a[3:] for a in self.get_names() if a.startswith(dotext)] + for alias in self.aliases: + if alias.startswith(text): + commands.append(alias) + if commands: + return commands + else: + return self._complete_expression(text, line, begidx, endidx) + def _complete_location(self, text, line, begidx, endidx): # Complete a file/module/function location for break/tbreak/clear. if line.strip().endswith((':', ',')): @@ -704,6 +717,10 @@ def _complete_expression(self, text, line, begidx, endidx): # complete builtins, and they clutter the namespace quite heavily, so we # leave them out. ns = {**self.curframe.f_globals, **self.curframe_locals} + if text.startswith("$"): + # Complete convenience variables + conv_vars = self.curframe.f_globals.get('__pdb_convenience_variables', {}) + return [f"${name}" for name in conv_vars if name.startswith(text[1:])] if '.' in text: # Walk an attribute chain up to the last part, similar to what # rlcompleter does. This will bail if any of the parts are not From 4f23a981bf678e7284c1be80f94717c22001e389 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 18:41:53 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2023-10-16-18-41-51.gh-issue-110944.CmUKXo.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2023-10-16-18-41-51.gh-issue-110944.CmUKXo.rst diff --git a/Misc/NEWS.d/next/Library/2023-10-16-18-41-51.gh-issue-110944.CmUKXo.rst b/Misc/NEWS.d/next/Library/2023-10-16-18-41-51.gh-issue-110944.CmUKXo.rst new file mode 100644 index 00000000000000..ec9ca5a11f1ac8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-10-16-18-41-51.gh-issue-110944.CmUKXo.rst @@ -0,0 +1 @@ +Support alias and convenience vars for :mod:`pdb` completion From 3771b9354027b29d1cdc124ae57921bc6448b529 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Fri, 3 Nov 2023 13:57:39 -0700 Subject: [PATCH 3/5] Add readline tests for pdb completion --- Lib/test/support/pty_helper.py | 60 ++++++++++++++++++++++++++++++++++ Lib/test/test_pdb.py | 41 +++++++++++++++++++++++ Lib/test/test_readline.py | 55 +------------------------------ 3 files changed, 102 insertions(+), 54 deletions(-) create mode 100644 Lib/test/support/pty_helper.py diff --git a/Lib/test/support/pty_helper.py b/Lib/test/support/pty_helper.py new file mode 100644 index 00000000000000..11037d22516448 --- /dev/null +++ b/Lib/test/support/pty_helper.py @@ -0,0 +1,60 @@ +""" +Helper to run a script in a pseudo-terminal. +""" +import os +import selectors +import subprocess +import sys +from contextlib import ExitStack +from errno import EIO + +from test.support.import_helper import import_module + +def run_pty(script, input=b"dummy input\r", env=None): + pty = import_module('pty') + output = bytearray() + [master, slave] = pty.openpty() + args = (sys.executable, '-c', script) + proc = subprocess.Popen(args, stdin=slave, stdout=slave, stderr=slave, env=env) + os.close(slave) + with ExitStack() as cleanup: + cleanup.enter_context(proc) + def terminate(proc): + try: + proc.terminate() + except ProcessLookupError: + # Workaround for Open/Net BSD bug (Issue 16762) + pass + cleanup.callback(terminate, proc) + cleanup.callback(os.close, master) + # Avoid using DefaultSelector and PollSelector. Kqueue() does not + # work with pseudo-terminals on OS X < 10.9 (Issue 20365) and Open + # BSD (Issue 20667). Poll() does not work with OS X 10.6 or 10.4 + # either (Issue 20472). Hopefully the file descriptor is low enough + # to use with select(). + sel = cleanup.enter_context(selectors.SelectSelector()) + sel.register(master, selectors.EVENT_READ | selectors.EVENT_WRITE) + os.set_blocking(master, False) + while True: + for [_, events] in sel.select(): + if events & selectors.EVENT_READ: + try: + chunk = os.read(master, 0x10000) + except OSError as err: + # Linux raises EIO when slave is closed (Issue 5380) + if err.errno != EIO: + raise + chunk = b"" + if not chunk: + return output + output.extend(chunk) + if events & selectors.EVENT_WRITE: + try: + input = input[os.write(master, input):] + except OSError as err: + # Apparently EIO means the slave was closed + if err.errno != EIO: + raise + input = b"" # Stop writing + if not input: + sel.modify(master, selectors.EVENT_READ) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index ff677aeb795442..f0d57407be3f13 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -15,6 +15,8 @@ from io import StringIO from test import support from test.support import os_helper +from test.support.import_helper import import_module +from test.support.pty_helper import run_pty # This little helper class is essential for testing pdb under doctest. from test.test_doctest import _FakeInput from unittest.mock import patch @@ -3117,6 +3119,45 @@ def test_checkline_is_not_executable(self): self.assertFalse(db.checkline(os_helper.TESTFN, lineno)) +@support.requires_subprocess() +class PdbTestReadline(unittest.TestCase): + def setUpClass(): + # Ensure that the readline module is loaded + # If this fails, the test is skipped because SkipTest will be raised + import_module('readline') + + def test_basic_completion(self): + script = textwrap.dedent(""" + import pdb; pdb.Pdb().set_trace() + print('hello') + """) + + input = b"co\t\tntin\t\n" + + output = run_pty(script, input) + + self.assertIn(b'cont', output) + self.assertIn(b'condition', output) + self.assertIn(b'continue', output) + + def test_expression_completion(self): + script = textwrap.dedent(""" + value = "speci" + import pdb; pdb.Pdb().set_trace() + """) + + input = b"val\t + 'al'\n" + input += b"p val\t + 'es'\n" + input += b"$_fra\t\n" + input += b"c\n" + + output = run_pty(script, input) + + self.assertIn(b'special', output) + self.assertIn(b'species', output) + self.assertIn(b'$_frame', output) + + def load_tests(loader, tests, pattern): from test import test_pdb tests.addTest(doctest.DocTestSuite(test_pdb)) diff --git a/Lib/test/test_readline.py b/Lib/test/test_readline.py index 59dbef90380053..835280f2281cde 100644 --- a/Lib/test/test_readline.py +++ b/Lib/test/test_readline.py @@ -1,18 +1,15 @@ """ Very minimal unittests for parts of the readline module. """ -from contextlib import ExitStack -from errno import EIO import locale import os -import selectors -import subprocess import sys import tempfile import unittest from test.support import verbose from test.support.import_helper import import_module from test.support.os_helper import unlink, temp_dir, TESTFN +from test.support.pty_helper import run_pty from test.support.script_helper import assert_python_ok # Skip tests if there is no readline module @@ -304,55 +301,5 @@ def test_history_size(self): self.assertEqual(lines[-1].strip(), b"last input") -def run_pty(script, input=b"dummy input\r", env=None): - pty = import_module('pty') - output = bytearray() - [master, slave] = pty.openpty() - args = (sys.executable, '-c', script) - proc = subprocess.Popen(args, stdin=slave, stdout=slave, stderr=slave, env=env) - os.close(slave) - with ExitStack() as cleanup: - cleanup.enter_context(proc) - def terminate(proc): - try: - proc.terminate() - except ProcessLookupError: - # Workaround for Open/Net BSD bug (Issue 16762) - pass - cleanup.callback(terminate, proc) - cleanup.callback(os.close, master) - # Avoid using DefaultSelector and PollSelector. Kqueue() does not - # work with pseudo-terminals on OS X < 10.9 (Issue 20365) and Open - # BSD (Issue 20667). Poll() does not work with OS X 10.6 or 10.4 - # either (Issue 20472). Hopefully the file descriptor is low enough - # to use with select(). - sel = cleanup.enter_context(selectors.SelectSelector()) - sel.register(master, selectors.EVENT_READ | selectors.EVENT_WRITE) - os.set_blocking(master, False) - while True: - for [_, events] in sel.select(): - if events & selectors.EVENT_READ: - try: - chunk = os.read(master, 0x10000) - except OSError as err: - # Linux raises EIO when slave is closed (Issue 5380) - if err.errno != EIO: - raise - chunk = b"" - if not chunk: - return output - output.extend(chunk) - if events & selectors.EVENT_WRITE: - try: - input = input[os.write(master, input):] - except OSError as err: - # Apparently EIO means the slave was closed - if err.errno != EIO: - raise - input = b"" # Stop writing - if not input: - sel.modify(master, selectors.EVENT_READ) - - if __name__ == "__main__": unittest.main() From f023848c24928815916590af54cb3693b6b8cc78 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Fri, 3 Nov 2023 16:10:32 -0700 Subject: [PATCH 4/5] Disable test with libedit --- Lib/test/test_pdb.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index f0d57407be3f13..4ea6618841bfd6 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -3124,7 +3124,9 @@ class PdbTestReadline(unittest.TestCase): def setUpClass(): # Ensure that the readline module is loaded # If this fails, the test is skipped because SkipTest will be raised - import_module('readline') + readline = import_module('readline') + if readline.__doc__ and "libedit" in readline.__doc__: + raise unittest.SkipTest("libedit readline is not supported for pdb") def test_basic_completion(self): script = textwrap.dedent(""" From b655b946f33cf3b4342dad6e01a84cf0a6f53b70 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Mon, 13 Nov 2023 12:38:52 -0800 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Petr Viktorin --- Lib/pdb.py | 3 +-- Lib/test/test_pdb.py | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 2b456799477c31..ed78d749a47fa8 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -689,8 +689,7 @@ def set_convenience_variable(self, frame, name, value): def completenames(self, text, line, begidx, endidx): # Overwrite completenames() of cmd so for the command completion, # if no current command matches, check for expressions as well - dotext = 'do_' + text - commands = [a[3:] for a in self.get_names() if a.startswith(dotext)] + commands = super().completenames(text, line, begidx, endidx) for alias in self.aliases: if alias.startswith(text): commands.append(alias) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 17cfebd935e65c..67a4053a2ac8bc 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -3295,9 +3295,13 @@ def test_expression_completion(self): import pdb; pdb.Pdb().set_trace() """) + # Complete: value + 'al' input = b"val\t + 'al'\n" + # Complete: p value + 'es' input += b"p val\t + 'es'\n" + # Complete: $_frame input += b"$_fra\t\n" + # Continue input += b"c\n" output = run_pty(script, input)