From 1a98aa322930741ca727411082f2c2c4b1f1fa5e Mon Sep 17 00:00:00 2001 From: Tan Long Date: Tue, 24 Jun 2025 16:41:31 +0800 Subject: [PATCH 01/13] Fix keeping part of previous commands when scrolling history --- Lib/sqlite3/__main__.py | 4 ++-- Lib/test/test_sqlite3/test_cli.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 35344ecceff526..f6935d10f03aa6 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -132,8 +132,8 @@ def main(*args): theme = get_theme() s = theme.syntax - sys.ps1 = f"{s.prompt}sqlite> {s.reset}" - sys.ps2 = f"{s.prompt} ... {s.reset}" + sys.ps1 = f"\001{s.prompt}\002sqlite> \001{s.reset}\002" + sys.ps2 = f"\001{s.prompt}\002 ... \001{s.reset}\002" con = sqlite3.connect(args.filename, isolation_level=None) try: diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 720fa3c4c1ea8b..4c963713fc1606 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -202,8 +202,8 @@ def test_interact_on_disk_file(self): def test_color(self): with unittest.mock.patch("_colorize.can_colorize", return_value=True): out, err = self.run_cli(commands="TEXT\n") - self.assertIn("\x1b[1;35msqlite> \x1b[0m", out) - self.assertIn("\x1b[1;35m ... \x1b[0m\x1b", out) + self.assertIn("\001\x1b[1;35m\002sqlite> \001\x1b[0m\002", out) + self.assertIn("\001\x1b[1;35m\002 ... \001\x1b[0m\002\001\x1b", out) out, err = self.run_cli(commands=("sel;",)) self.assertIn('\x1b[1;35mOperationalError (SQLITE_ERROR)\x1b[0m: ' '\x1b[35mnear "sel": syntax error\x1b[0m', err) From 609f8b032dc1d79219935c18c81847ae5853f6a0 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Tue, 24 Jun 2025 19:07:23 +0800 Subject: [PATCH 02/13] blurb add --- .../next/Library/2025-06-24-19-07-18.gh-issue-135883.38cePA.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-06-24-19-07-18.gh-issue-135883.38cePA.rst diff --git a/Misc/NEWS.d/next/Library/2025-06-24-19-07-18.gh-issue-135883.38cePA.rst b/Misc/NEWS.d/next/Library/2025-06-24-19-07-18.gh-issue-135883.38cePA.rst new file mode 100644 index 00000000000000..df0f21f4d29224 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-24-19-07-18.gh-issue-135883.38cePA.rst @@ -0,0 +1 @@ +Fix sqlite3 CLI keeping part of previous commands when scrolling history. From 7c1c2abec37301ec55a6caf02689220c515d51cc Mon Sep 17 00:00:00 2001 From: Tan Long Date: Tue, 24 Jun 2025 19:26:27 +0800 Subject: [PATCH 03/13] fix test --- Lib/sqlite3/__main__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index f6935d10f03aa6..68970f5fcf5d47 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -132,8 +132,12 @@ def main(*args): theme = get_theme() s = theme.syntax - sys.ps1 = f"\001{s.prompt}\002sqlite> \001{s.reset}\002" - sys.ps2 = f"\001{s.prompt}\002 ... \001{s.reset}\002" + if s.prompt: + sys.ps1 = f"\001{s.prompt}\002sqlite> \001{s.reset}\002" + sys.ps2 = f"\001{s.prompt}\002 ... \001{s.reset}\002" + else: + sys.ps1 = f"sqlite> " + sys.ps2 = f" ... " con = sqlite3.connect(args.filename, isolation_level=None) try: From cf89e537835f590dbc9304ea411e59edb05b71bc Mon Sep 17 00:00:00 2001 From: Tan Long Date: Tue, 24 Jun 2025 20:39:33 +0800 Subject: [PATCH 04/13] improve news entry --- .../Library/2025-06-24-19-07-18.gh-issue-135883.38cePA.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-06-24-19-07-18.gh-issue-135883.38cePA.rst b/Misc/NEWS.d/next/Library/2025-06-24-19-07-18.gh-issue-135883.38cePA.rst index df0f21f4d29224..8f3efceaae1d91 100644 --- a/Misc/NEWS.d/next/Library/2025-06-24-19-07-18.gh-issue-135883.38cePA.rst +++ b/Misc/NEWS.d/next/Library/2025-06-24-19-07-18.gh-issue-135883.38cePA.rst @@ -1 +1,2 @@ -Fix sqlite3 CLI keeping part of previous commands when scrolling history. +Fix :mod:`sqlite3`'s :ref:`interactive shell ` keeping part of +previous commands when scrolling history. From bebca3ec4b188ceb6e66cfd333357562fdd44def Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 2 Mar 2026 23:27:12 +0800 Subject: [PATCH 05/13] add comment about \001 and \002' --- Lib/sqlite3/__main__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 68970f5fcf5d47..c18880d1a4389a 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -132,12 +132,12 @@ def main(*args): theme = get_theme() s = theme.syntax - if s.prompt: - sys.ps1 = f"\001{s.prompt}\002sqlite> \001{s.reset}\002" - sys.ps2 = f"\001{s.prompt}\002 ... \001{s.reset}\002" - else: - sys.ps1 = f"sqlite> " - sys.ps2 = f" ... " + # Use RL_PROMPT_START_IGNORE (\001) and RL_PROMPT_END_IGNORE (\002) to + # bracket non-printing characters. This tells readline to ignore them when + # calculating screen space for redisplay during history scrolling. See + # https://stackoverflow.com/a/9468954 for more details. + sys.ps1 = f"\001{s.prompt}\002sqlite> \001{s.reset}\002" + sys.ps2 = f"\001{s.prompt}\002 ... \001{s.reset}\002" con = sqlite3.connect(args.filename, isolation_level=None) try: From d71ca9845d094e17d1125648c19241741ba10073 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 2 Mar 2026 23:33:57 +0800 Subject: [PATCH 06/13] Revert "add comment about \001 and \002'" This reverts commit bebca3ec4b188ceb6e66cfd333357562fdd44def. --- Lib/sqlite3/__main__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index c18880d1a4389a..68970f5fcf5d47 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -132,12 +132,12 @@ def main(*args): theme = get_theme() s = theme.syntax - # Use RL_PROMPT_START_IGNORE (\001) and RL_PROMPT_END_IGNORE (\002) to - # bracket non-printing characters. This tells readline to ignore them when - # calculating screen space for redisplay during history scrolling. See - # https://stackoverflow.com/a/9468954 for more details. - sys.ps1 = f"\001{s.prompt}\002sqlite> \001{s.reset}\002" - sys.ps2 = f"\001{s.prompt}\002 ... \001{s.reset}\002" + if s.prompt: + sys.ps1 = f"\001{s.prompt}\002sqlite> \001{s.reset}\002" + sys.ps2 = f"\001{s.prompt}\002 ... \001{s.reset}\002" + else: + sys.ps1 = f"sqlite> " + sys.ps2 = f" ... " con = sqlite3.connect(args.filename, isolation_level=None) try: From b657a1f96802bbce40b757d3d10965e5e58cbd64 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 2 Mar 2026 23:34:37 +0800 Subject: [PATCH 07/13] add comment about \001 and \002 --- Lib/sqlite3/__main__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 68970f5fcf5d47..6da3bebe255456 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -133,11 +133,15 @@ def main(*args): s = theme.syntax if s.prompt: + # Use RL_PROMPT_START_IGNORE (\001) and RL_PROMPT_END_IGNORE (\002) to + # bracket non-printing characters. This tells readline to ignore them + # when calculating screen space for redisplay during history scrolling. + # See https://stackoverflow.com/a/9468954 for more details. sys.ps1 = f"\001{s.prompt}\002sqlite> \001{s.reset}\002" sys.ps2 = f"\001{s.prompt}\002 ... \001{s.reset}\002" else: - sys.ps1 = f"sqlite> " - sys.ps2 = f" ... " + sys.ps1 = "sqlite> " + sys.ps2 = " ... " con = sqlite3.connect(args.filename, isolation_level=None) try: From 608543c846c2cf85083ae01da775e87a9138166c Mon Sep 17 00:00:00 2001 From: Tan Long Date: Tue, 3 Mar 2026 00:03:13 +0800 Subject: [PATCH 08/13] Remove the conditional --- Lib/sqlite3/__main__.py | 16 ++++++---------- Lib/test/test_sqlite3/test_cli.py | 10 +++++----- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 6da3bebe255456..44c5e44b9ae9b7 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -132,16 +132,12 @@ def main(*args): theme = get_theme() s = theme.syntax - if s.prompt: - # Use RL_PROMPT_START_IGNORE (\001) and RL_PROMPT_END_IGNORE (\002) to - # bracket non-printing characters. This tells readline to ignore them - # when calculating screen space for redisplay during history scrolling. - # See https://stackoverflow.com/a/9468954 for more details. - sys.ps1 = f"\001{s.prompt}\002sqlite> \001{s.reset}\002" - sys.ps2 = f"\001{s.prompt}\002 ... \001{s.reset}\002" - else: - sys.ps1 = "sqlite> " - sys.ps2 = " ... " + # Use RL_PROMPT_START_IGNORE (\001) and RL_PROMPT_END_IGNORE (\002) to + # bracket non-printing characters. This tells readline to ignore them + # when calculating screen space for redisplay during history scrolling. + # See https://stackoverflow.com/a/9468954 for more details. + sys.ps1 = f"\001{s.prompt}\002sqlite> \001{s.reset}\002" + sys.ps2 = f"\001{s.prompt}\002 ... \001{s.reset}\002" con = sqlite3.connect(args.filename, isolation_level=None) try: diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 4c963713fc1606..54aa25b74aef5d 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -80,8 +80,8 @@ def test_cli_on_disk_db(self): @force_not_colorized_test_class class InteractiveSession(unittest.TestCase): MEMORY_DB_MSG = "Connected to a transient in-memory database" - PS1 = "sqlite> " - PS2 = "... " + PS1 = "\001\002sqlite> \001\002" + PS2 = "\001\002 ... \001\002" def run_cli(self, *args, commands=()): with ( @@ -212,7 +212,7 @@ def test_color(self): @requires_subprocess() @force_not_colorized_test_class class Completion(unittest.TestCase): - PS1 = "sqlite> " + PS1_NO_COLOR = "sqlite> " @classmethod def setUpClass(cls): @@ -260,7 +260,7 @@ def test_complete_no_match(self): lines = output.decode().splitlines() indices = ( i for i, line in enumerate(lines, 1) - if line.startswith(f"{self.PS1}xyzzy") + if line.startswith(f"{self.PS1_NO_COLOR}xyzzy") ) line_num = next(indices, -1) self.assertNotEqual(line_num, -1) @@ -296,7 +296,7 @@ def test_complete_no_input(self): lines = output.decode().splitlines() indices = [ i for i, line in enumerate(lines) - if line.startswith(self.PS1) + if line.startswith(self.PS1_NO_COLOR) ] self.assertEqual(len(indices), 2) start, end = indices From ff4bcf6ef1c8c8f0c365a4bb9445aa74920117fb Mon Sep 17 00:00:00 2001 From: Tan Long Date: Tue, 3 Mar 2026 01:30:34 +0800 Subject: [PATCH 09/13] Fix tests --- Lib/test/test_sqlite3/test_cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index b105315311faf1..141641edde63bf 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -288,7 +288,7 @@ def test_complete_table_indexes_triggers_views(self): output = self.write_input(input_) lines = output.decode().splitlines() indices = [i for i, line in enumerate(lines) - if line.startswith(self.PS1)] + if line.startswith(self.PS1_NO_COLOR)] start, end = indices[-3], indices[-2] candidates = [l.strip() for l in lines[start+1:end]] self.assertEqual(candidates, @@ -327,7 +327,7 @@ def test_complete_columns(self): output = self.write_input(input_) lines = output.decode().splitlines() indices = [ - i for i, line in enumerate(lines) if line.startswith(self.PS1) + i for i, line in enumerate(lines) if line.startswith(self.PS1_NO_COLOR) ] start, end = indices[-3], indices[-2] candidates = [l.strip() for l in lines[start+1:end]] @@ -365,7 +365,7 @@ def test_complete_schemata(self): output = self.write_input(input_) lines = output.decode().splitlines() indices = [ - i for i, line in enumerate(lines) if line.startswith(self.PS1) + i for i, line in enumerate(lines) if line.startswith(self.PS1_NO_COLOR) ] start, end = indices[-4], indices[-3] candidates = [l.strip() for l in lines[start+1:end]] From 06cf0a7b7734239c0f5a35a7b17461c69f2a5847 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Wed, 4 Mar 2026 00:01:00 +0800 Subject: [PATCH 10/13] Add comment why use plain prompt for test assertion in test_cli.Completion --- Lib/test/test_sqlite3/test_cli.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 141641edde63bf..c65b4074a4bf9c 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -212,7 +212,12 @@ def test_color(self): @requires_subprocess() @force_not_colorized_test_class class Completion(unittest.TestCase): - PS1_NO_COLOR = "sqlite> " + # run_pty() creates a real terminal environment, where sqlite3 CLI + # SqliteInteractiveConsole invokes GNU Readline for input. Readline's + # _rl_strip_prompt() strips \001 and \002 from the output, so test + # assertions use the plain prompt. See + # https://cgit.git.savannah.gnu.org/cgit/readline.git/tree/display.c + PS1 = "sqlite> " @classmethod def setUpClass(cls): @@ -288,7 +293,7 @@ def test_complete_table_indexes_triggers_views(self): output = self.write_input(input_) lines = output.decode().splitlines() indices = [i for i, line in enumerate(lines) - if line.startswith(self.PS1_NO_COLOR)] + if line.startswith(self.PS1)] start, end = indices[-3], indices[-2] candidates = [l.strip() for l in lines[start+1:end]] self.assertEqual(candidates, @@ -327,7 +332,7 @@ def test_complete_columns(self): output = self.write_input(input_) lines = output.decode().splitlines() indices = [ - i for i, line in enumerate(lines) if line.startswith(self.PS1_NO_COLOR) + i for i, line in enumerate(lines) if line.startswith(self.PS1) ] start, end = indices[-3], indices[-2] candidates = [l.strip() for l in lines[start+1:end]] @@ -365,7 +370,7 @@ def test_complete_schemata(self): output = self.write_input(input_) lines = output.decode().splitlines() indices = [ - i for i, line in enumerate(lines) if line.startswith(self.PS1_NO_COLOR) + i for i, line in enumerate(lines) if line.startswith(self.PS1) ] start, end = indices[-4], indices[-3] candidates = [l.strip() for l in lines[start+1:end]] @@ -385,7 +390,7 @@ def test_complete_no_match(self): lines = output.decode().splitlines() indices = ( i for i, line in enumerate(lines, 1) - if line.startswith(f"{self.PS1_NO_COLOR}xyzzy") + if line.startswith(f"{self.PS1}xyzzy") ) line_num = next(indices, -1) self.assertNotEqual(line_num, -1) @@ -419,7 +424,7 @@ def test_complete_no_input(self): lines = output.decode().splitlines() indices = [ i for i, line in enumerate(lines) - if line.startswith(self.PS1_NO_COLOR) + if line.startswith(self.PS1) ] self.assertEqual(len(indices), 2) start, end = indices From c71bb45b06f095e8153ecd7682c46b0e6616cb25 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Wed, 4 Mar 2026 00:03:56 +0800 Subject: [PATCH 11/13] Update Lib/test/test_sqlite3/test_cli.py Co-authored-by: Erlend E. Aasland --- Lib/test/test_sqlite3/test_cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 141641edde63bf..f44f2fb5c57cc4 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -202,8 +202,8 @@ def test_interact_on_disk_file(self): def test_color(self): with unittest.mock.patch("_colorize.can_colorize", return_value=True): out, err = self.run_cli(commands="TEXT\n") - self.assertIn("\001\x1b[1;35m\002sqlite> \001\x1b[0m\002", out) - self.assertIn("\001\x1b[1;35m\002 ... \001\x1b[0m\002\001\x1b", out) + self.assertIn("\x01\x1b[1;35m\x02sqlite> \x01\x1b[0m\x02", out) + self.assertIn("\x01\x1b[1;35m\x02 ... \x01\x1b[0m\x02\x01\x1b", out) out, err = self.run_cli(commands=("sel;",)) self.assertIn('\x1b[1;35mOperationalError (SQLITE_ERROR)\x1b[0m: ' '\x1b[35mnear "sel": syntax error\x1b[0m', err) From 93ba7134b00719b2c5236aab7e12f0b8497a33e6 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Wed, 4 Mar 2026 00:04:18 +0800 Subject: [PATCH 12/13] Update Lib/sqlite3/__main__.py Co-authored-by: Erlend E. Aasland --- Lib/sqlite3/__main__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index cb379bcfe65564..8805442b69e080 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -136,7 +136,6 @@ def main(*args): # Use RL_PROMPT_START_IGNORE (\001) and RL_PROMPT_END_IGNORE (\002) to # bracket non-printing characters. This tells readline to ignore them # when calculating screen space for redisplay during history scrolling. - # See https://stackoverflow.com/a/9468954 for more details. sys.ps1 = f"\001{s.prompt}\002sqlite> \001{s.reset}\002" sys.ps2 = f"\001{s.prompt}\002 ... \001{s.reset}\002" From 212e63bc26724e29016031c8a9de8dfd10395e00 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Wed, 4 Mar 2026 00:05:07 +0800 Subject: [PATCH 13/13] Drop link from comment --- Lib/test/test_sqlite3/test_cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index c65b4074a4bf9c..6c62c9fa7f5fb7 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -215,8 +215,7 @@ class Completion(unittest.TestCase): # run_pty() creates a real terminal environment, where sqlite3 CLI # SqliteInteractiveConsole invokes GNU Readline for input. Readline's # _rl_strip_prompt() strips \001 and \002 from the output, so test - # assertions use the plain prompt. See - # https://cgit.git.savannah.gnu.org/cgit/readline.git/tree/display.c + # assertions use the plain prompt. PS1 = "sqlite> " @classmethod