Skip to content

Commit 14ea66d

Browse files
Draft is done!
1 parent c23190e commit 14ea66d

File tree

2 files changed

+156
-43
lines changed

2 files changed

+156
-43
lines changed

Lib/pdb.py

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -349,17 +349,34 @@ def get_default_backend():
349349
return _default_backend
350350

351351

352+
def _pyrepl_available():
353+
"""return whether pdb should use _pyrepl for input"""
354+
if not os.getenv("PYTHON_BASIC_REPL"):
355+
from _pyrepl.main import CAN_USE_PYREPL
356+
357+
return CAN_USE_PYREPL
358+
return False
359+
360+
352361
class PdbPyReplInput:
353-
def __init__(self, pdb_instance, prompt):
354-
from _pyrepl.readline import _setup
362+
def __init__(self, pdb_instance, stdin, stdout, prompt):
363+
import _pyrepl.readline
364+
355365
self.pdb_instance = pdb_instance
356366
self.prompt = prompt
357367
self.console = code.InteractiveConsole()
358-
_setup({})
368+
if not (os.isatty(stdin.fileno())):
369+
raise ValueError("stdin is not a TTY")
370+
self.readline_wrapper = _pyrepl.readline._ReadlineWrapper(
371+
f_in=stdin.fileno(),
372+
f_out=stdout.fileno(),
373+
config=_pyrepl.readline.ReadlineConfig(
374+
completer_delims=frozenset(' \t\n`@#%^&*()=+[{]}\\|;:\'",<>?')
375+
)
376+
)
359377

360378
def readline(self):
361379
from _pyrepl.simple_interact import _more_lines
362-
from _pyrepl.readline import get_completer, multiline_input, set_completer
363380

364381
def more_lines(text):
365382
cmd, _, line = self.pdb_instance.parseline(text)
@@ -371,13 +388,48 @@ def more_lines(text):
371388
return _more_lines(self.console, text)
372389

373390
try:
374-
pyrepl_completer = get_completer()
375-
set_completer(self.pdb_instance.complete)
376-
return multiline_input(more_lines, self.prompt, '... ') + '\n'
391+
pyrepl_completer = self.readline_wrapper.get_completer()
392+
self.readline_wrapper.set_completer(self.complete)
393+
return (
394+
self.readline_wrapper.multiline_input(
395+
more_lines,
396+
self.prompt,
397+
'... ' + ' ' * (len(self.prompt) - 4)
398+
) + '\n'
399+
)
377400
except EOFError:
378401
return 'EOF'
379402
finally:
380-
set_completer(pyrepl_completer)
403+
self.readline_wrapper.set_completer(pyrepl_completer)
404+
405+
def complete(self, text, state):
406+
"""
407+
This function is very similar to cmd.Cmd.complete.
408+
However, cmd.Cmd.complete assumes that we use readline module, but
409+
pyrepl does not use it.
410+
"""
411+
if state == 0:
412+
origline = self.readline_wrapper.get_line_buffer()
413+
line = origline.lstrip()
414+
stripped = len(origline) - len(line)
415+
begidx = self.readline_wrapper.get_begidx() - stripped
416+
endidx = self.readline_wrapper.get_endidx() - stripped
417+
if begidx>0:
418+
cmd, args, foo = self.pdb_instance.parseline(line)
419+
if not cmd:
420+
compfunc = self.pdb_instance.completedefault
421+
else:
422+
try:
423+
compfunc = getattr(self.pdb_instance, 'complete_' + cmd)
424+
except AttributeError:
425+
compfunc = self.pdb_instance.completedefault
426+
else:
427+
compfunc = self.pdb_instance.completenames
428+
self.completion_matches = compfunc(text, line, begidx, endidx)
429+
try:
430+
return self.completion_matches[state]
431+
except IndexError:
432+
return None
381433

382434

383435
class Pdb(bdb.Bdb, cmd.Cmd):
@@ -414,10 +466,12 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
414466
except ImportError:
415467
pass
416468

417-
if self.use_rawinput and stdin is None:
418-
self.pyrepl_input = PdbPyReplInput(self, self.prompt)
419-
else:
420-
self.pyrepl_input = None
469+
self.pyrepl_input = None
470+
if _pyrepl_available():
471+
try:
472+
self.pyrepl_input = PdbPyReplInput(self, self.stdin, self.stdout, self.prompt)
473+
except Exception:
474+
pass
421475
self.allow_kbdint = False
422476
self.nosigint = nosigint
423477
# Consider these characters as part of the command so when the users type
@@ -2422,10 +2476,20 @@ def do_interact(self, arg):
24222476
contains all the (global and local) names found in the current scope.
24232477
"""
24242478
ns = {**self.curframe.f_globals, **self.curframe.f_locals}
2425-
with self._enable_rlcompleter(ns):
2426-
console = _PdbInteractiveConsole(ns, message=self.message)
2427-
console.interact(banner="*pdb interact start*",
2428-
exitmsg="*exit from pdb interact command*")
2479+
console = _PdbInteractiveConsole(ns, message=self.message)
2480+
if self.pyrepl_input is not None:
2481+
from _pyrepl.simple_interact import run_multiline_interactive_console
2482+
self.message("*pdb interact start*")
2483+
try:
2484+
run_multiline_interactive_console(console)
2485+
except SystemExit:
2486+
pass
2487+
self.message("*exit from pdb interact command*")
2488+
else:
2489+
with self._enable_rlcompleter(ns):
2490+
console = _PdbInteractiveConsole(ns, message=self.message)
2491+
console.interact(banner="*pdb interact start*",
2492+
exitmsg="*exit from pdb interact command*")
24292493

24302494
def do_alias(self, arg):
24312495
"""alias [name [command]]

Lib/test/test_pdb.py

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io
77
import os
88
import pdb
9+
import re
910
import sys
1011
import types
1112
import codecs
@@ -5006,6 +5007,20 @@ def setUpClass(cls):
50065007
if readline.backend == "editline":
50075008
raise unittest.SkipTest("libedit readline is not supported for pdb")
50085009

5010+
def _run_pty(self, script, input, env=None):
5011+
if env is None:
5012+
# By default, we use basic repl for the test.
5013+
# Subclass can overwrite this method and set env to use advanced REPL
5014+
env = os.environ | {'PYTHON_BASIC_REPL': '1'}
5015+
output = run_pty(script, input, env=env)
5016+
# filter all control characters
5017+
# Strip ANSI CSI sequences (good enough for most REPL/prompt output)
5018+
output = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", output.decode("utf-8"))
5019+
return output
5020+
5021+
def _pyrepl_available(self):
5022+
return pdb._pyrepl_available()
5023+
50095024
def test_basic_completion(self):
50105025
script = textwrap.dedent("""
50115026
import pdb; pdb.Pdb().set_trace()
@@ -5017,12 +5032,12 @@ def test_basic_completion(self):
50175032
# then add ntin and complete 'contin' to 'continue'
50185033
input = b"co\t\tntin\t\n"
50195034

5020-
output = run_pty(script, input)
5035+
output = self._run_pty(script, input)
50215036

5022-
self.assertIn(b'commands', output)
5023-
self.assertIn(b'condition', output)
5024-
self.assertIn(b'continue', output)
5025-
self.assertIn(b'hello!', output)
5037+
self.assertIn('commands', output)
5038+
self.assertIn('condition', output)
5039+
self.assertIn('continue', output)
5040+
self.assertIn('hello!', output)
50265041

50275042
def test_expression_completion(self):
50285043
script = textwrap.dedent("""
@@ -5039,11 +5054,11 @@ def test_expression_completion(self):
50395054
# Continue
50405055
input += b"c\n"
50415056

5042-
output = run_pty(script, input)
5057+
output = self._run_pty(script, input)
50435058

5044-
self.assertIn(b'special', output)
5045-
self.assertIn(b'species', output)
5046-
self.assertIn(b'$_frame', output)
5059+
self.assertIn('special', output)
5060+
self.assertIn('species', output)
5061+
self.assertIn('$_frame', output)
50475062

50485063
def test_builtin_completion(self):
50495064
script = textwrap.dedent("""
@@ -5057,9 +5072,9 @@ def test_builtin_completion(self):
50575072
# Continue
50585073
input += b"c\n"
50595074

5060-
output = run_pty(script, input)
5075+
output = self._run_pty(script, input)
50615076

5062-
self.assertIn(b'special', output)
5077+
self.assertIn('special', output)
50635078

50645079
def test_convvar_completion(self):
50655080
script = textwrap.dedent("""
@@ -5075,10 +5090,10 @@ def test_convvar_completion(self):
50755090
# Continue
50765091
input += b"c\n"
50775092

5078-
output = run_pty(script, input)
5093+
output = self._run_pty(script, input)
50795094

5080-
self.assertIn(b'<frame at 0x', output)
5081-
self.assertIn(b'102', output)
5095+
self.assertIn('<frame at 0x', output)
5096+
self.assertIn('102', output)
50825097

50835098
def test_local_namespace(self):
50845099
script = textwrap.dedent("""
@@ -5094,9 +5109,9 @@ def f():
50945109
# Continue
50955110
input += b"c\n"
50965111

5097-
output = run_pty(script, input)
5112+
output = self._run_pty(script, input)
50985113

5099-
self.assertIn(b'I love Python', output)
5114+
self.assertIn('I love Python', output)
51005115

51015116
@unittest.skipIf(sys.platform.startswith('freebsd'),
51025117
'\\x08 is not interpreted as backspace on FreeBSD')
@@ -5116,9 +5131,9 @@ def test_multiline_auto_indent(self):
51165131
input += b"f(-21-21)\n"
51175132
input += b"c\n"
51185133

5119-
output = run_pty(script, input)
5134+
output = self._run_pty(script, input)
51205135

5121-
self.assertIn(b'42', output)
5136+
self.assertIn('42', output)
51225137

51235138
def test_multiline_completion(self):
51245139
script = textwrap.dedent("""
@@ -5134,9 +5149,9 @@ def test_multiline_completion(self):
51345149
input += b"fun\t()\n"
51355150
input += b"c\n"
51365151

5137-
output = run_pty(script, input)
5152+
output = self._run_pty(script, input)
51385153

5139-
self.assertIn(b'42', output)
5154+
self.assertIn('42', output)
51405155

51415156
@unittest.skipIf(sys.platform.startswith('freebsd'),
51425157
'\\x08 is not interpreted as backspace on FreeBSD')
@@ -5162,10 +5177,10 @@ def func():
51625177
c
51635178
""").encode()
51645179

5165-
output = run_pty(script, input)
5180+
output = self._run_pty(script, input)
51665181

5167-
self.assertIn(b'5', output)
5168-
self.assertNotIn(b'Error', output)
5182+
self.assertIn('5', output)
5183+
self.assertNotIn('Error', output)
51695184

51705185
def test_interact_completion(self):
51715186
script = textwrap.dedent("""
@@ -5189,11 +5204,45 @@ def test_interact_completion(self):
51895204
# continue
51905205
input += b"c\n"
51915206

5192-
output = run_pty(script, input)
5207+
output = self._run_pty(script, input)
5208+
5209+
self.assertIn("'disp' is not defined", output)
5210+
self.assertIn('special', output)
5211+
self.assertIn('84', output)
5212+
5213+
5214+
@unittest.skipIf(not pdb._pyrepl_available(), "pyrepl is not available")
5215+
class PdbTestReadlinePyREPL(PdbTestReadline):
5216+
def _run_pty(self, script, input):
5217+
# Override the env to make sure pyrepl is used in this test class
5218+
return super()._run_pty(script, input, env={**os.environ})
5219+
5220+
def test_pyrepl_used(self):
5221+
script = textwrap.dedent("""
5222+
import pdb
5223+
db = pdb.Pdb()
5224+
print(db.pyrepl_input)
5225+
""")
5226+
input = b""
5227+
output = self._run_pty(script, input)
5228+
self.assertIn('PdbPyReplInput', output)
5229+
5230+
def test_pyrepl_multiline_change(self):
5231+
script = textwrap.dedent("""
5232+
import pdb; pdb.Pdb().set_trace()
5233+
""")
5234+
5235+
input = b"def f():\n"
5236+
# Auto-indent should work here
5237+
input += b"return x"
5238+
# The following command tries to add the argument x in f()
5239+
# up, left, left (in the parenthesis now), "x", down, down (at the end)
5240+
input += b"\x1bOA\x1bOD\x1bODx\x1bOB\x1bOB\n\n"
5241+
input += b"f(40 + 2)\n"
5242+
input += b"c\n"
51935243

5194-
self.assertIn(b"'disp' is not defined", output)
5195-
self.assertIn(b'special', output)
5196-
self.assertIn(b'84', output)
5244+
output = self._run_pty(script, input)
5245+
self.assertIn('42', output)
51975246

51985247

51995248
def load_tests(loader, tests, pattern):

0 commit comments

Comments
 (0)