diff --git a/Doc/make.bat b/Doc/make.bat index 64a42257c92571..99f0d5c44f0098 100644 --- a/Doc/make.bat +++ b/Doc/make.bat @@ -13,7 +13,7 @@ if not defined SPHINXBUILD ( %PYTHON% -c "import sphinx" > nul 2> nul if errorlevel 1 ( echo Installing sphinx with %PYTHON% - %PYTHON% -m pip install -r pylock.toml + %PYTHON% -m pip install -r requirements.txt if errorlevel 1 exit /B ) set SPHINXBUILD=%PYTHON% -c "import sphinx.cmd.build, sys; sys.exit(sphinx.cmd.build.main())" diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 3c2c7a7e399d09..56b2553a401920 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1046,6 +1046,13 @@ os.path (Contributed by Petr Viktorin for :cve:`2025-4517`.) +pdb +--- + +* Use the new interactive shell as the default input shell for :mod:`pdb`. + (Contributed by Tian Gao in :gh:`145379`.) + + pickle ------ diff --git a/Lib/pdb.py b/Lib/pdb.py index 7b08d2bb70183d..c4bc0020646b0d 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -318,12 +318,34 @@ def namespace(self): class _PdbInteractiveConsole(code.InteractiveConsole): - def __init__(self, ns, message): + def __init__(self, ns=None, message=None): self._message = message super().__init__(locals=ns, local_exit=True) def write(self, data): - self._message(data, end='') + if self._message is not None: + self._message(data, end='') + else: + super().write(data) + + def more_lines(self, text): + # Generic Python multi-line completeness heuristic. + # Strips pyrepl's trailing auto-indent before compiling. + # This should be functionally identical to simple_interact._more_lines + src = text.rstrip(" \t") + n = len(src) + if n > 0 and text[n-1] == '\n': + text = src + try: + code_obj = self.compile(text, "", "single") + except (OverflowError, SyntaxError, ValueError): + lines = text.splitlines(keepends=True) + if len(lines) == 1: + return False + last = lines[-1] + return ((last.startswith((" ", "\t")) or last.strip() != "") + and not last.endswith("\n")) + return code_obj is None # Interaction prompt line will separate file and call info from code @@ -352,6 +374,96 @@ def get_default_backend(): return _default_backend +def _pyrepl_available(): + """return whether pdb should use _pyrepl for input""" + if not os.getenv("PYTHON_BASIC_REPL"): + CAN_USE_PYREPL = False + else: + try: + from _pyrepl.main import CAN_USE_PYREPL + except ModuleNotFoundError: + CAN_USE_PYREPL = False + return CAN_USE_PYREPL + + +class PdbPyReplInput: + def __init__(self, pdb_instance, stdin, stdout, prompt): + import _pyrepl.readline + + self.pdb_instance = pdb_instance + self.prompt = prompt + self.console = _PdbInteractiveConsole() + if not (os.isatty(stdin.fileno())): + raise ValueError("stdin is not a TTY") + self.readline_wrapper = _pyrepl.readline._ReadlineWrapper( + f_in=stdin.fileno(), + f_out=stdout.fileno(), + config=_pyrepl.readline.ReadlineConfig( + completer_delims=frozenset(' \t\n`@#%^&*()=+[{]}\\|;:\'",<>?') + ) + ) + + def readline(self): + + def more_lines(text): + if text.strip() == "\x1a": + # Ctrl + Z raises EOFError to quit pdb + # This is similarly handled in simple_interact.py + raise EOFError + cmd, _, line = self.pdb_instance.parseline(text) + if not line or not cmd: + return False + func = getattr(self.pdb_instance, 'do_' + cmd, None) + if func is not None: + return False + return self.console.more_lines(text) + + try: + pyrepl_completer = self.readline_wrapper.get_completer() + self.readline_wrapper.set_completer(self.complete) + multiline = ( + self.readline_wrapper.multiline_input( + more_lines, + self.prompt, + '... ' + ' ' * (len(self.prompt) - 4) + ) + '\n' + ) + return multiline + except EOFError: + return 'EOF' + finally: + self.readline_wrapper.set_completer(pyrepl_completer) + + def complete(self, text, state): + """ + This function is very similar to cmd.Cmd.complete. + However, cmd.Cmd.complete assumes that we use readline module, but + pyrepl does not use it. + """ + if state == 0: + origline = self.readline_wrapper.get_line_buffer() + line = origline.lstrip() + stripped = len(origline) - len(line) + begidx = self.readline_wrapper.get_begidx() - stripped + endidx = self.readline_wrapper.get_endidx() - stripped + if begidx > 0: + cmd, args, foo = self.pdb_instance.parseline(line) + if not cmd: + compfunc = self.pdb_instance.completedefault + else: + try: + compfunc = getattr(self.pdb_instance, 'complete_' + cmd) + except AttributeError: + compfunc = self.pdb_instance.completedefault + else: + compfunc = self.pdb_instance.completenames + self.completion_matches = compfunc(text, line, begidx, endidx) + try: + return self.completion_matches[state] + except IndexError: + return None + + class Pdb(bdb.Bdb, cmd.Cmd): _previous_sigint_handler = None @@ -386,6 +498,12 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, except ImportError: pass + self.pyrepl_input = None + if _pyrepl_available(): + try: + self.pyrepl_input = PdbPyReplInput(self, self.stdin, self.stdout, self.prompt) + except Exception: + pass self.allow_kbdint = False self.nosigint = nosigint # Consider these characters as part of the command so when the users type @@ -624,6 +742,31 @@ def user_exception(self, frame, exc_info): self.message('%s%s' % (prefix, self._format_exc(exc_value))) self.interaction(frame, exc_traceback) + @contextmanager + def _replace_attribute(self, attrs): + original_attrs = {} + for attr, value in attrs.items(): + original_attrs[attr] = getattr(self, attr) + setattr(self, attr, value) + try: + yield + finally: + for attr, value in original_attrs.items(): + setattr(self, attr, value) + + @contextmanager + def _maybe_use_pyrepl_as_stdin(self): + if self.pyrepl_input is None: + yield + return + + with self._replace_attribute({ + 'stdin': self.pyrepl_input, + 'use_rawinput': False, + 'prompt': '', + }): + yield + # General interaction function def _cmdloop(self): while True: @@ -631,7 +774,8 @@ def _cmdloop(self): # keyboard interrupts allow for an easy way to cancel # the current command, so allow them during interactive input self.allow_kbdint = True - self.cmdloop() + with self._maybe_use_pyrepl_as_stdin(): + self.cmdloop() self.allow_kbdint = False break except KeyboardInterrupt: @@ -2364,10 +2508,21 @@ def do_interact(self, arg): contains all the (global and local) names found in the current scope. """ ns = {**self.curframe.f_globals, **self.curframe.f_locals} - with self._enable_rlcompleter(ns): - console = _PdbInteractiveConsole(ns, message=self.message) - console.interact(banner="*pdb interact start*", - exitmsg="*exit from pdb interact command*") + console = _PdbInteractiveConsole(ns, message=self.message) + banner = "*pdb interact start*" + exitmsg = "*exit from pdb interact command*" + if self.pyrepl_input is not None: + from _pyrepl.simple_interact import run_multiline_interactive_console + self.message(banner) + try: + run_multiline_interactive_console(console) + except SystemExit: + pass + self.message(exitmsg) + else: + with self._enable_rlcompleter(ns): + console.interact(banner=banner, + exitmsg=exitmsg) def do_alias(self, arg): """alias [name [command]] diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 79643587a60002..7118dfeed9faee 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -3919,6 +3919,38 @@ def testfunc(args): expected = TIER2_THRESHOLD * (5.0 / Fraction(4)) self.assertAlmostEqual(res, float(expected)) + def test_float_truediv_partial_float_no_stack_underflow(self): + # gh-149049: a speculative _GUARD_*_FLOAT for a partially-float + # truediv/remainder must not drop the original _BINARY_OP. + def truediv(args): + n, = args + nan = float("nan") + def victim(a=0, b=nan, c=2): + return (a + b) / c + for _ in range(n): + victim() + + def remainder(args): + n, = args + nan = float("nan") + def victim(a=0, b=nan, c=2): + return (a + b) % c + for _ in range(n): + victim() + + for testfunc in (truediv, remainder): + with self.subTest(op=testfunc.__name__): + # Iterations must be high enough that the buggy trace + # is not only built but executed (where it underflows). + _, ex = self._run_with_optimizer( + testfunc, (TIER2_THRESHOLD * 10,)) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertTrue( + "_GUARD_TOS_FLOAT" in uops or "_GUARD_NOS_FLOAT" in uops, + uops, + ) + def test_int_add_inplace_unique_lhs(self): # a * b produces a unique compact int; adding c reuses it in place def testfunc(args): diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 0e23cd6604379c..c5171f3388c965 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -6,6 +6,7 @@ import io import os import pdb +import re import sys import types import codecs @@ -5006,6 +5007,20 @@ def setUpClass(cls): if readline.backend == "editline": raise unittest.SkipTest("libedit readline is not supported for pdb") + def _run_pty(self, script, input, env=None): + if env is None: + # By default, we use basic repl for the test. + # Subclass can overwrite this method and set env to use advanced REPL + env = os.environ | {'PYTHON_BASIC_REPL': '1'} + output = run_pty(script, input, env=env) + # filter all control characters + # Strip ANSI CSI sequences (good enough for most REPL/prompt output) + output = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", output.decode("utf-8")) + return output + + def _pyrepl_available(self): + return pdb._pyrepl_available() + def test_basic_completion(self): script = textwrap.dedent(""" import pdb; pdb.Pdb().set_trace() @@ -5017,12 +5032,12 @@ def test_basic_completion(self): # then add ntin and complete 'contin' to 'continue' input = b"co\t\tntin\t\n" - output = run_pty(script, input) + output = self._run_pty(script, input) - self.assertIn(b'commands', output) - self.assertIn(b'condition', output) - self.assertIn(b'continue', output) - self.assertIn(b'hello!', output) + self.assertIn('commands', output) + self.assertIn('condition', output) + self.assertIn('continue', output) + self.assertIn('hello!', output) def test_expression_completion(self): script = textwrap.dedent(""" @@ -5039,11 +5054,11 @@ def test_expression_completion(self): # Continue input += b"c\n" - output = run_pty(script, input) + output = self._run_pty(script, input) - self.assertIn(b'special', output) - self.assertIn(b'species', output) - self.assertIn(b'$_frame', output) + self.assertIn('special', output) + self.assertIn('species', output) + self.assertIn('$_frame', output) def test_builtin_completion(self): script = textwrap.dedent(""" @@ -5057,9 +5072,9 @@ def test_builtin_completion(self): # Continue input += b"c\n" - output = run_pty(script, input) + output = self._run_pty(script, input) - self.assertIn(b'special', output) + self.assertIn('special', output) def test_convvar_completion(self): script = textwrap.dedent(""" @@ -5075,10 +5090,10 @@ def test_convvar_completion(self): # Continue input += b"c\n" - output = run_pty(script, input) + output = self._run_pty(script, input) - self.assertIn(b'ste_coroutine) { + Py_DECREF(generator_entry); return 0; } + Py_DECREF(generator_entry); location loc = LOC(func); diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index ae9e19341441ea..15d4d0bc1818f5 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -293,6 +293,7 @@ dummy_func(void) { || oparg == NB_INPLACE_TRUE_DIVIDE); bool is_remainder = (oparg == NB_REMAINDER || oparg == NB_INPLACE_REMAINDER); + int emit_op = _BINARY_OP; // Promote probable-float operands to known floats via speculative // guards. _RECORD_TOS_TYPE / _RECORD_NOS_TYPE in the BINARY_OP macro // record the observed operand type during tracing, which @@ -318,17 +319,17 @@ dummy_func(void) { } if (is_truediv && lhs_float && rhs_float) { if (PyJitRef_IsUnique(lhs)) { - ADD_OP(_BINARY_OP_TRUEDIV_FLOAT_INPLACE, 0, 0); + emit_op = _BINARY_OP_TRUEDIV_FLOAT_INPLACE; l = sym_new_null(ctx); r = rhs; } else if (PyJitRef_IsUnique(rhs)) { - ADD_OP(_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT, 0, 0); + emit_op = _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT; l = lhs; r = sym_new_null(ctx); } else { - ADD_OP(_BINARY_OP_TRUEDIV_FLOAT, 0, 0); + emit_op = _BINARY_OP_TRUEDIV_FLOAT; l = lhs; r = rhs; } @@ -382,6 +383,7 @@ dummy_func(void) { else { res = PyJitRef_MakeUnique(sym_new_type(ctx, &PyFloat_Type)); } + ADD_OP(emit_op, oparg, 0); } op(_BINARY_OP_ADD_INT, (left, right -- res, l, r)) { diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index d48f38a95f7b16..b09aca910fc1ee 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -5242,6 +5242,7 @@ || oparg == NB_INPLACE_TRUE_DIVIDE); bool is_remainder = (oparg == NB_REMAINDER || oparg == NB_INPLACE_REMAINDER); + int emit_op = _BINARY_OP; if (is_truediv || is_remainder) { if (!sym_has_type(rhs) && sym_get_probable_type(rhs) == &PyFloat_Type) { @@ -5258,17 +5259,17 @@ } if (is_truediv && lhs_float && rhs_float) { if (PyJitRef_IsUnique(lhs)) { - ADD_OP(_BINARY_OP_TRUEDIV_FLOAT_INPLACE, 0, 0); + emit_op = _BINARY_OP_TRUEDIV_FLOAT_INPLACE; l = sym_new_null(ctx); r = rhs; } else if (PyJitRef_IsUnique(rhs)) { - ADD_OP(_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT, 0, 0); + emit_op = _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT; l = lhs; r = sym_new_null(ctx); } else { - ADD_OP(_BINARY_OP_TRUEDIV_FLOAT, 0, 0); + emit_op = _BINARY_OP_TRUEDIV_FLOAT; l = lhs; r = rhs; } @@ -5304,6 +5305,7 @@ else { res = PyJitRef_MakeUnique(sym_new_type(ctx, &PyFloat_Type)); } + ADD_OP(emit_op, oparg, 0); CHECK_STACK_BOUNDS(1); stack_pointer[-2] = res; stack_pointer[-1] = l; diff --git a/Tools/msi/README.txt b/Tools/msi/README.txt index 8ae156450d5240..4b3de9e82f6f1a 100644 --- a/Tools/msi/README.txt +++ b/Tools/msi/README.txt @@ -528,4 +528,3 @@ explicitly handled by the installer. Python packages installed later using a tool like pip will not be removed. Some components may be installed by other installers and these will not be removed if another product has a dependency on them. -