From 771f6176761621b9e5725a7936e3e09a71e45844 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 9 May 2025 19:41:10 +0800 Subject: [PATCH 1/7] gh-133439: Fix dot commands with trailing spaces are mistaken for multi-line sqlite statements in the sqlite3 command-line interface (GH-133440) (cherry picked from commit ebd4881db2e8448b238d8ca2f6fcf331826132dd) --- Lib/sqlite3/__main__.py | 30 ++++++++++------- Lib/test/test_sqlite3/test_cli.py | 32 +++++++++++++++++++ ...-05-05-22-11-24.gh-issue-133439.LpmyFz.rst | 2 ++ 3 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-05-05-22-11-24.gh-issue-133439.LpmyFz.rst diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index b93b84384a0925..87a80a6f952505 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -48,17 +48,25 @@ def runsource(self, source, filename="", symbol="single"): Return True if more input is needed; buffering is done automatically. Return False is input is a complete statement ready for execution. """ - match source: - case ".version": - print(f"{sqlite3.sqlite_version}") - case ".help": - print("Enter SQL code and press enter.") - case ".quit": - sys.exit(0) - case _: - if not sqlite3.complete_statement(source): - return True - execute(self._cur, source) + if not source or source.isspace(): + return False + if source[0] == ".": + match source[1:].strip(): + case "version": + print(f"{sqlite3.sqlite_version}") + case "help": + print("Enter SQL code and press enter.") + case "quit": + sys.exit(0) + case "": + pass + case _ as unknown: + self.write("Error: unknown command or invalid arguments:" + f' "{unknown}".\n') + else: + if not sqlite3.complete_statement(source): + return True + execute(self._cur, source) return False diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 303f9e03b5383f..9f7717fb627113 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -108,6 +108,38 @@ def test_interact_version(self): self.assertEqual(out.count(self.PS2), 0) self.assertIn(sqlite3.sqlite_version, out) + def test_interact_empty_source(self): + out, err = self.run_cli(commands=("", " ")) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertEndsWith(out, self.PS1) + self.assertEqual(out.count(self.PS1), 3) + self.assertEqual(out.count(self.PS2), 0) + + def test_interact_dot_commands_unknown(self): + out, err = self.run_cli(commands=(".unknown_command", )) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertEndsWith(out, self.PS1) + self.assertEqual(out.count(self.PS1), 2) + self.assertEqual(out.count(self.PS2), 0) + self.assertIn("Error", err) + # test "unknown_command" is pointed out in the error message + self.assertIn("unknown_command", err) + + def test_interact_dot_commands_empty(self): + out, err = self.run_cli(commands=(".")) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertEndsWith(out, self.PS1) + self.assertEqual(out.count(self.PS1), 2) + self.assertEqual(out.count(self.PS2), 0) + + def test_interact_dot_commands_with_whitespaces(self): + out, err = self.run_cli(commands=(".version ", ". version")) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertEqual(out.count(sqlite3.sqlite_version + "\n"), 2) + self.assertEndsWith(out, self.PS1) + self.assertEqual(out.count(self.PS1), 3) + self.assertEqual(out.count(self.PS2), 0) + def test_interact_valid_sql(self): out, err = self.run_cli(commands=("SELECT 1;",)) self.assertIn(self.MEMORY_DB_MSG, err) diff --git a/Misc/NEWS.d/next/Library/2025-05-05-22-11-24.gh-issue-133439.LpmyFz.rst b/Misc/NEWS.d/next/Library/2025-05-05-22-11-24.gh-issue-133439.LpmyFz.rst new file mode 100644 index 00000000000000..e0a3ce98bf7158 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-05-22-11-24.gh-issue-133439.LpmyFz.rst @@ -0,0 +1,2 @@ +Fix dot commands with trailing spaces are mistaken for multi-line SQL +statements in the sqlite3 command-line interface. From 62d471923537967dd51c99605bf0a8a69ace8956 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 9 May 2025 23:12:23 +0800 Subject: [PATCH 2/7] Fix tests --- Lib/test/test_sqlite3/test_cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 9f7717fb627113..30e381281b13ff 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -111,14 +111,14 @@ def test_interact_version(self): def test_interact_empty_source(self): out, err = self.run_cli(commands=("", " ")) self.assertIn(self.MEMORY_DB_MSG, err) - self.assertEndsWith(out, self.PS1) + self.assertTrue(out.endswith(self.PS1)) self.assertEqual(out.count(self.PS1), 3) self.assertEqual(out.count(self.PS2), 0) def test_interact_dot_commands_unknown(self): out, err = self.run_cli(commands=(".unknown_command", )) self.assertIn(self.MEMORY_DB_MSG, err) - self.assertEndsWith(out, self.PS1) + self.assertTrue(out.endswith(self.PS1)) self.assertEqual(out.count(self.PS1), 2) self.assertEqual(out.count(self.PS2), 0) self.assertIn("Error", err) @@ -128,7 +128,7 @@ def test_interact_dot_commands_unknown(self): def test_interact_dot_commands_empty(self): out, err = self.run_cli(commands=(".")) self.assertIn(self.MEMORY_DB_MSG, err) - self.assertEndsWith(out, self.PS1) + self.assertTrue(out.endswith(self.PS1)) self.assertEqual(out.count(self.PS1), 2) self.assertEqual(out.count(self.PS2), 0) @@ -136,7 +136,7 @@ def test_interact_dot_commands_with_whitespaces(self): out, err = self.run_cli(commands=(".version ", ". version")) self.assertIn(self.MEMORY_DB_MSG, err) self.assertEqual(out.count(sqlite3.sqlite_version + "\n"), 2) - self.assertEndsWith(out, self.PS1) + self.assertTrue(out.endswith(self.PS1)) self.assertEqual(out.count(self.PS1), 3) self.assertEqual(out.count(self.PS2), 0) From 23e4b3cef21d8fe59d5e364bca880a5e46cfd98d Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sat, 10 May 2025 16:55:42 +0800 Subject: [PATCH 3/7] Revert "Fix tests" This reverts commit 62d471923537967dd51c99605bf0a8a69ace8956. --- Lib/test/test_sqlite3/test_cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 30e381281b13ff..9f7717fb627113 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -111,14 +111,14 @@ def test_interact_version(self): def test_interact_empty_source(self): out, err = self.run_cli(commands=("", " ")) self.assertIn(self.MEMORY_DB_MSG, err) - self.assertTrue(out.endswith(self.PS1)) + self.assertEndsWith(out, self.PS1) self.assertEqual(out.count(self.PS1), 3) self.assertEqual(out.count(self.PS2), 0) def test_interact_dot_commands_unknown(self): out, err = self.run_cli(commands=(".unknown_command", )) self.assertIn(self.MEMORY_DB_MSG, err) - self.assertTrue(out.endswith(self.PS1)) + self.assertEndsWith(out, self.PS1) self.assertEqual(out.count(self.PS1), 2) self.assertEqual(out.count(self.PS2), 0) self.assertIn("Error", err) @@ -128,7 +128,7 @@ def test_interact_dot_commands_unknown(self): def test_interact_dot_commands_empty(self): out, err = self.run_cli(commands=(".")) self.assertIn(self.MEMORY_DB_MSG, err) - self.assertTrue(out.endswith(self.PS1)) + self.assertEndsWith(out, self.PS1) self.assertEqual(out.count(self.PS1), 2) self.assertEqual(out.count(self.PS2), 0) @@ -136,7 +136,7 @@ def test_interact_dot_commands_with_whitespaces(self): out, err = self.run_cli(commands=(".version ", ". version")) self.assertIn(self.MEMORY_DB_MSG, err) self.assertEqual(out.count(sqlite3.sqlite_version + "\n"), 2) - self.assertTrue(out.endswith(self.PS1)) + self.assertEndsWith(out, self.PS1) self.assertEqual(out.count(self.PS1), 3) self.assertEqual(out.count(self.PS2), 0) From 2cd00c4887ea747ed533a4ecf68ca144f438d94f Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sat, 10 May 2025 16:58:29 +0800 Subject: [PATCH 4/7] Use `assertEndsWith` from `test.support.testcase.ExtraAssertions` --- Lib/test/test_sqlite3/test_cli.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 9f7717fb627113..63da3dec911604 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -4,10 +4,11 @@ from sqlite3.__main__ import main as cli from test.support.os_helper import TESTFN, unlink +from test.support.testcase import ExtraAssertions from test.support import captured_stdout, captured_stderr, captured_stdin -class CommandLineInterface(unittest.TestCase): +class CommandLineInterface(unittest.TestCase, ExtraAssertions): def _do_test(self, *args, expect_success=True): with ( @@ -88,14 +89,14 @@ def test_interact(self): out, err = self.run_cli() self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn(self.MEMORY_DB_MSG, err) - self.assertTrue(out.endswith(self.PS1)) + self.assertEndsWith(out, self.PS1) self.assertEqual(out.count(self.PS1), 1) self.assertEqual(out.count(self.PS2), 0) def test_interact_quit(self): out, err = self.run_cli(commands=(".quit",)) self.assertIn(self.MEMORY_DB_MSG, err) - self.assertTrue(out.endswith(self.PS1)) + self.assertEndsWith(out, self.PS1) self.assertEqual(out.count(self.PS1), 1) self.assertEqual(out.count(self.PS2), 0) @@ -103,7 +104,7 @@ def test_interact_version(self): out, err = self.run_cli(commands=(".version",)) self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn(sqlite3.sqlite_version + "\n", out) - self.assertTrue(out.endswith(self.PS1)) + self.assertEndsWith(out, self.PS1) self.assertEqual(out.count(self.PS1), 2) self.assertEqual(out.count(self.PS2), 0) self.assertIn(sqlite3.sqlite_version, out) @@ -144,14 +145,14 @@ def test_interact_valid_sql(self): out, err = self.run_cli(commands=("SELECT 1;",)) self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn("(1,)\n", out) - self.assertTrue(out.endswith(self.PS1)) + self.assertEndsWith(out, self.PS1) self.assertEqual(out.count(self.PS1), 2) self.assertEqual(out.count(self.PS2), 0) def test_interact_incomplete_multiline_sql(self): out, err = self.run_cli(commands=("SELECT 1",)) self.assertIn(self.MEMORY_DB_MSG, err) - self.assertTrue(out.endswith(self.PS2)) + self.assertEndsWith(out, self.PS2) self.assertEqual(out.count(self.PS1), 1) self.assertEqual(out.count(self.PS2), 1) @@ -160,7 +161,7 @@ def test_interact_valid_multiline_sql(self): self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn(self.PS2, out) self.assertIn("(1,)\n", out) - self.assertTrue(out.endswith(self.PS1)) + self.assertEndsWith(out, self.PS1) self.assertEqual(out.count(self.PS1), 2) self.assertEqual(out.count(self.PS2), 1) @@ -168,7 +169,7 @@ def test_interact_invalid_sql(self): out, err = self.run_cli(commands=("sel;",)) self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn("OperationalError (SQLITE_ERROR)", err) - self.assertTrue(out.endswith(self.PS1)) + self.assertEndsWith(out, self.PS1) self.assertEqual(out.count(self.PS1), 2) self.assertEqual(out.count(self.PS2), 0) @@ -177,7 +178,7 @@ def test_interact_on_disk_file(self): out, err = self.run_cli(TESTFN, commands=("CREATE TABLE t(t);",)) self.assertIn(TESTFN, err) - self.assertTrue(out.endswith(self.PS1)) + self.assertEndsWith(out, self.PS1) out, _ = self.run_cli(TESTFN, commands=("SELECT count(t) FROM t;",)) self.assertIn("(0,)\n", out) From 2f74e65c339a9a99f9616ee4118fca9e7232e45f Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sat, 10 May 2025 17:12:09 +0800 Subject: [PATCH 5/7] Fix tests: mix in `ExtraAssertions` to `InteractiveSession` --- Lib/test/test_sqlite3/test_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 63da3dec911604..897fb5a9369b38 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -62,7 +62,7 @@ def test_cli_on_disk_db(self): self.assertIn("(0,)", out) -class InteractiveSession(unittest.TestCase): +class InteractiveSession(unittest.TestCase, ExtraAssertions): MEMORY_DB_MSG = "Connected to a transient in-memory database" PS1 = "sqlite> " PS2 = "... " From 22d2b27bffff017413f2633c2d51dca14f50f38a Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sat, 10 May 2025 17:15:52 +0800 Subject: [PATCH 6/7] Revert changes unrelated to the original PR --- Lib/test/test_sqlite3/test_cli.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 897fb5a9369b38..c7659655cc6a95 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -89,14 +89,14 @@ def test_interact(self): out, err = self.run_cli() self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn(self.MEMORY_DB_MSG, err) - self.assertEndsWith(out, self.PS1) + self.assertTrue(out.endswith(self.PS1)) self.assertEqual(out.count(self.PS1), 1) self.assertEqual(out.count(self.PS2), 0) def test_interact_quit(self): out, err = self.run_cli(commands=(".quit",)) self.assertIn(self.MEMORY_DB_MSG, err) - self.assertEndsWith(out, self.PS1) + self.assertTrue(out.endswith(self.PS1)) self.assertEqual(out.count(self.PS1), 1) self.assertEqual(out.count(self.PS2), 0) @@ -104,7 +104,7 @@ def test_interact_version(self): out, err = self.run_cli(commands=(".version",)) self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn(sqlite3.sqlite_version + "\n", out) - self.assertEndsWith(out, self.PS1) + self.assertTrue(out.endswith(self.PS1)) self.assertEqual(out.count(self.PS1), 2) self.assertEqual(out.count(self.PS2), 0) self.assertIn(sqlite3.sqlite_version, out) @@ -145,14 +145,14 @@ def test_interact_valid_sql(self): out, err = self.run_cli(commands=("SELECT 1;",)) self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn("(1,)\n", out) - self.assertEndsWith(out, self.PS1) + self.assertTrue(out.endswith(self.PS1)) self.assertEqual(out.count(self.PS1), 2) self.assertEqual(out.count(self.PS2), 0) def test_interact_incomplete_multiline_sql(self): out, err = self.run_cli(commands=("SELECT 1",)) self.assertIn(self.MEMORY_DB_MSG, err) - self.assertEndsWith(out, self.PS2) + self.assertTrue(out.endswith(self.PS2)) self.assertEqual(out.count(self.PS1), 1) self.assertEqual(out.count(self.PS2), 1) @@ -161,7 +161,7 @@ def test_interact_valid_multiline_sql(self): self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn(self.PS2, out) self.assertIn("(1,)\n", out) - self.assertEndsWith(out, self.PS1) + self.assertTrue(out.endswith(self.PS1)) self.assertEqual(out.count(self.PS1), 2) self.assertEqual(out.count(self.PS2), 1) @@ -169,7 +169,7 @@ def test_interact_invalid_sql(self): out, err = self.run_cli(commands=("sel;",)) self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn("OperationalError (SQLITE_ERROR)", err) - self.assertEndsWith(out, self.PS1) + self.assertTrue(out.endswith(self.PS1)) self.assertEqual(out.count(self.PS1), 2) self.assertEqual(out.count(self.PS2), 0) @@ -178,7 +178,7 @@ def test_interact_on_disk_file(self): out, err = self.run_cli(TESTFN, commands=("CREATE TABLE t(t);",)) self.assertIn(TESTFN, err) - self.assertEndsWith(out, self.PS1) + self.assertTrue(out.endswith(self.PS1)) out, _ = self.run_cli(TESTFN, commands=("SELECT count(t) FROM t;",)) self.assertIn("(0,)\n", out) From 889992501692c5dfefaea29d8d47fec06bfc4e4f Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sat, 10 May 2025 17:44:35 +0800 Subject: [PATCH 7/7] Remove `ExtraAssertions` from `CommandLineInterface` --- Lib/test/test_sqlite3/test_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index c7659655cc6a95..a461328b4ecd05 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -8,7 +8,7 @@ from test.support import captured_stdout, captured_stderr, captured_stdin -class CommandLineInterface(unittest.TestCase, ExtraAssertions): +class CommandLineInterface(unittest.TestCase): def _do_test(self, *args, expect_success=True): with (