From 1b96be3eb95509c43bcb583ef0cb891cfef46a19 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 5 May 2025 02:21:04 +0800 Subject: [PATCH 01/36] Support basic completion for sqlite3 command-line interface --- Lib/sqlite3/__main__.py | 63 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index c2fa23c46cf990..89f7409cd3a854 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -9,6 +9,7 @@ from argparse import ArgumentParser from code import InteractiveConsole +from contextlib import contextmanager from textwrap import dedent from _colorize import get_theme, theme_no_color @@ -79,6 +80,59 @@ def runsource(self, source, filename="", symbol="single"): return False +def _complete(text, state): + keywords = ["ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", + "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", + "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE", "CAST", + "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", + "CONSTRAINT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", + "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", + "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", + "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", + "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", + "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", + "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", + "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", + "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", + "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", + "MATCH", "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", + "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", "OR", + "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", + "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", + "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", + "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", + "ROLLBACK", "ROW", "ROWS", "SAVEPOINT", "SELECT", "SET", + "TABLE", "TEMP", "TEMPORARY", "THEN", "TIES", "TO", + "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", + "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", + "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT"] + options = [c + " " for c in keywords if c.startswith(text.upper())] + try: + return options[state] + except IndexError: + return None + +@contextmanager +def _enable_completer(): + try: + import readline + except ImportError: + yield + return + + old_completer = readline.get_completer() + try: + readline.set_completer(_complete) + if readline.backend == "editline": + # libedit uses "^I" instead of "tab" + command_string = "bind ^I rl_complete" + else: + command_string = "tab: complete" + readline.parse_and_bind(command_string) + yield + finally: + readline.set_completer(old_completer) + def main(*args): parser = ArgumentParser( description="Python sqlite3 CLI", @@ -136,12 +190,9 @@ def main(*args): execute(con, args.sql, suppress_errors=False, theme=theme) else: # No SQL provided; start the REPL. - console = SqliteInteractiveConsole(con, use_color=True) - try: - import readline # noqa: F401 - except ImportError: - pass - console.interact(banner, exitmsg="") + with _enable_completer(): + console = SqliteInteractiveConsole(con, use_color=True) + console.interact(banner, exitmsg="") finally: con.close() From 5e5087198aad4a0000428ac9efd4df50e791b945 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 5 May 2025 03:16:24 +0800 Subject: [PATCH 02/36] Add news entry --- .../next/Library/2025-05-05-03-14-08.gh-issue-133390.AuTggn.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-05-05-03-14-08.gh-issue-133390.AuTggn.rst diff --git a/Misc/NEWS.d/next/Library/2025-05-05-03-14-08.gh-issue-133390.AuTggn.rst b/Misc/NEWS.d/next/Library/2025-05-05-03-14-08.gh-issue-133390.AuTggn.rst new file mode 100644 index 00000000000000..3e0720b8e77690 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-05-03-14-08.gh-issue-133390.AuTggn.rst @@ -0,0 +1 @@ +Support completion for :mod:`sqlite3` command-line interface. From c1941cbdf8564e5bd580993e82099cb10d964ef2 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 5 May 2025 05:04:06 +0800 Subject: [PATCH 03/36] Move completion code to separate module --- Doc/whatsnew/3.14.rst | 3 + Lib/sqlite3/__main__.py | 58 +------------------ Lib/sqlite3/_completer.py | 58 +++++++++++++++++++ ...-05-05-03-14-08.gh-issue-133390.AuTggn.rst | 2 +- 4 files changed, 65 insertions(+), 56 deletions(-) create mode 100644 Lib/sqlite3/_completer.py diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index d7b3bac8d85f1f..edb5f99714e46d 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -2387,6 +2387,9 @@ sqlite3 it will now raise a :exc:`sqlite3.ProgrammingError`. (Contributed by Erlend E. Aasland in :gh:`118928` and :gh:`101693`.) +* Support keyword completion for :mod:`sqlite3` command-line interface. + (Contributed by Long Tan in :gh:`133393`.) + typing ------ diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 89f7409cd3a854..ad4d33843acaaf 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -9,10 +9,11 @@ from argparse import ArgumentParser from code import InteractiveConsole -from contextlib import contextmanager from textwrap import dedent from _colorize import get_theme, theme_no_color +from ._completer import enable_completer + def execute(c, sql, suppress_errors=True, theme=theme_no_color): """Helper that wraps execution of SQL code. @@ -80,59 +81,6 @@ def runsource(self, source, filename="", symbol="single"): return False -def _complete(text, state): - keywords = ["ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", - "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", - "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE", "CAST", - "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", - "CONSTRAINT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", - "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", - "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", - "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", - "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", - "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", - "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", - "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", - "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", - "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", - "MATCH", "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", - "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", "OR", - "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", - "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", - "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", - "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", - "ROLLBACK", "ROW", "ROWS", "SAVEPOINT", "SELECT", "SET", - "TABLE", "TEMP", "TEMPORARY", "THEN", "TIES", "TO", - "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", - "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", - "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT"] - options = [c + " " for c in keywords if c.startswith(text.upper())] - try: - return options[state] - except IndexError: - return None - -@contextmanager -def _enable_completer(): - try: - import readline - except ImportError: - yield - return - - old_completer = readline.get_completer() - try: - readline.set_completer(_complete) - if readline.backend == "editline": - # libedit uses "^I" instead of "tab" - command_string = "bind ^I rl_complete" - else: - command_string = "tab: complete" - readline.parse_and_bind(command_string) - yield - finally: - readline.set_completer(old_completer) - def main(*args): parser = ArgumentParser( description="Python sqlite3 CLI", @@ -190,7 +138,7 @@ def main(*args): execute(con, args.sql, suppress_errors=False, theme=theme) else: # No SQL provided; start the REPL. - with _enable_completer(): + with enable_completer(): console = SqliteInteractiveConsole(con, use_color=True) console.interact(banner, exitmsg="") finally: diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py new file mode 100644 index 00000000000000..2590cfccef44f9 --- /dev/null +++ b/Lib/sqlite3/_completer.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +from contextlib import contextmanager + + +def _complete(text, state): + keywords = ["ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", + "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", + "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE", "CAST", + "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", + "CONSTRAINT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", + "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", + "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", + "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", + "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", + "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", + "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", + "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", + "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", + "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", + "MATCH", "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", + "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", "OR", + "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", + "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", + "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", + "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", + "ROLLBACK", "ROW", "ROWS", "SAVEPOINT", "SELECT", "SET", + "TABLE", "TEMP", "TEMPORARY", "THEN", "TIES", "TO", + "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", + "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", + "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT"] + options = [c + " " for c in keywords if c.startswith(text.upper())] + try: + return options[state] + except IndexError: + return None + + +@contextmanager +def enable_completer(): + try: + import readline + except ImportError: + yield + return + + old_completer = readline.get_completer() + try: + readline.set_completer(_complete) + if readline.backend == "editline": + # libedit uses "^I" instead of "tab" + command_string = "bind ^I rl_complete" + else: + command_string = "tab: complete" + readline.parse_and_bind(command_string) + yield + finally: + readline.set_completer(old_completer) diff --git a/Misc/NEWS.d/next/Library/2025-05-05-03-14-08.gh-issue-133390.AuTggn.rst b/Misc/NEWS.d/next/Library/2025-05-05-03-14-08.gh-issue-133390.AuTggn.rst index 3e0720b8e77690..070e18948e4471 100644 --- a/Misc/NEWS.d/next/Library/2025-05-05-03-14-08.gh-issue-133390.AuTggn.rst +++ b/Misc/NEWS.d/next/Library/2025-05-05-03-14-08.gh-issue-133390.AuTggn.rst @@ -1 +1 @@ -Support completion for :mod:`sqlite3` command-line interface. +Support keyword completion for :mod:`sqlite3` command-line interface. From 47daca5cefcd06ca09f20481b129f298ab1a8334 Mon Sep 17 00:00:00 2001 From: "Tan, Long" Date: Mon, 5 May 2025 06:30:40 +0800 Subject: [PATCH 04/36] Update Lib/sqlite3/_completer.py Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --- Lib/sqlite3/_completer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py index 2590cfccef44f9..414b4705cf9704 100644 --- a/Lib/sqlite3/_completer.py +++ b/Lib/sqlite3/_completer.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - from contextlib import contextmanager From c54c2f62c8c4f54fe32556bf95452556672f802a Mon Sep 17 00:00:00 2001 From: "Tan, Long" Date: Mon, 5 May 2025 06:31:06 +0800 Subject: [PATCH 05/36] Update Doc/whatsnew/3.14.rst Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --- Doc/whatsnew/3.14.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index edb5f99714e46d..a2fea895648c51 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -2387,7 +2387,7 @@ sqlite3 it will now raise a :exc:`sqlite3.ProgrammingError`. (Contributed by Erlend E. Aasland in :gh:`118928` and :gh:`101693`.) -* Support keyword completion for :mod:`sqlite3` command-line interface. +* Support keyword completion in the :mod:`sqlite3` command-line interface. (Contributed by Long Tan in :gh:`133393`.) typing From 8fff49130246981e56c3095ca4c69e3e56d56ca1 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 5 May 2025 09:45:30 +0800 Subject: [PATCH 06/36] Add test --- Lib/test/test_sqlite3/test_cli.py | 35 +++++++++++++++++++ ...-05-05-03-14-08.gh-issue-133390.AuTggn.rst | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 37e0f74f688659..abeb95e2897a1c 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -1,14 +1,18 @@ """sqlite3 CLI tests.""" +import re import sqlite3 import unittest from sqlite3.__main__ import main as cli +from test.support.import_helper import import_module from test.support.os_helper import TESTFN, unlink +from test.support.pty_helper import run_pty from test.support import ( captured_stdout, captured_stderr, captured_stdin, force_not_colorized_test_class, + requires_subprocess, ) @@ -200,5 +204,36 @@ def test_color(self): self.assertIn('\x1b[1;35mOperationalError (SQLITE_ERROR)\x1b[0m: ' '\x1b[35mnear "sel": syntax error\x1b[0m', err) +@requires_subprocess() +class Completer(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Ensure that the readline module is loaded + # If this fails, the test is skipped because SkipTest will be raised + readline = import_module("readline") + if readline.backend == "editline": + raise unittest.SkipTest("libedit readline is not supported") + + def test_keyword_completion(self): + script = "from sqlite3.__main__ import main; main()" + # List candidates starting with 'S', there should be multiple matches. + # Then add 'EL' and complete 'SEL' to 'SELECT'. Quit console in the end + # to let run_pty() return. + input = b"S\t\tEL\t 1;\n.quit\n" + output = run_pty(script, input) + # Remove control sequences that colorize typed prefix 'S' + output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output) + self.assertIn(b"SELECT", output) + self.assertIn(b"SET", output) + self.assertIn(b"SAVEPOINT", output) + self.assertIn(b"(1,)", output) + + # Keywords are completed in upper case for even lower case user input + input = b"sel\t\t 1;\n.quit\n" + output = run_pty(script, input) + output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output) + self.assertIn(b"SELECT", output) + self.assertIn(b"(1,)", output) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2025-05-05-03-14-08.gh-issue-133390.AuTggn.rst b/Misc/NEWS.d/next/Library/2025-05-05-03-14-08.gh-issue-133390.AuTggn.rst index 070e18948e4471..38d5c311b1d437 100644 --- a/Misc/NEWS.d/next/Library/2025-05-05-03-14-08.gh-issue-133390.AuTggn.rst +++ b/Misc/NEWS.d/next/Library/2025-05-05-03-14-08.gh-issue-133390.AuTggn.rst @@ -1 +1 @@ -Support keyword completion for :mod:`sqlite3` command-line interface. +Support keyword completion in the :mod:`sqlite3` command-line interface. From a7668050c006a330bdbbbd8ecf6e67846b651fb2 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 5 May 2025 12:46:48 +0800 Subject: [PATCH 07/36] Move keyword list to module level --- Lib/sqlite3/_completer.py | 54 ++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py index 414b4705cf9704..a8351842d88394 100644 --- a/Lib/sqlite3/_completer.py +++ b/Lib/sqlite3/_completer.py @@ -1,33 +1,35 @@ from contextlib import contextmanager +_keywords = ["ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", + "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", + "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE", "CAST", + "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", + "CONSTRAINT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", + "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", + "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", + "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", + "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", + "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", + "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", + "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", + "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", + "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", + "MATCH", "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", + "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", "OR", + "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", + "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", + "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", + "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", + "ROLLBACK", "ROW", "ROWS", "SAVEPOINT", "SELECT", "SET", + "TABLE", "TEMP", "TEMPORARY", "THEN", "TIES", "TO", + "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", + "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", + "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT"] + + def _complete(text, state): - keywords = ["ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", - "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", - "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE", "CAST", - "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", - "CONSTRAINT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", - "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", - "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", - "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", - "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", - "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", - "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", - "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", - "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", - "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", - "MATCH", "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", - "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", "OR", - "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", - "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", - "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", - "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", - "ROLLBACK", "ROW", "ROWS", "SAVEPOINT", "SELECT", "SET", - "TABLE", "TEMP", "TEMPORARY", "THEN", "TIES", "TO", - "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", - "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", - "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT"] - options = [c + " " for c in keywords if c.startswith(text.upper())] + options = [c + " " for c in _keywords if c.startswith(text.upper())] try: return options[state] except IndexError: From da550144c68331cdcd51b1409fde33a1fbe8e81b Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 5 May 2025 23:30:57 +0800 Subject: [PATCH 08/36] Remove whatsnew entry from 3.14 --- Doc/whatsnew/3.14.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index a2fea895648c51..d7b3bac8d85f1f 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -2387,9 +2387,6 @@ sqlite3 it will now raise a :exc:`sqlite3.ProgrammingError`. (Contributed by Erlend E. Aasland in :gh:`118928` and :gh:`101693`.) -* Support keyword completion in the :mod:`sqlite3` command-line interface. - (Contributed by Long Tan in :gh:`133393`.) - typing ------ From ca587e05da085848cee0d98fd97ee4ca6a6dabb4 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 8 May 2025 06:17:18 +0800 Subject: [PATCH 09/36] Avoid regeneration of candidates. Store them when state is 0 and returns them one by one on subsequent calls http://tiswww.case.edu/php/chet/readline/readline.html#How-Completing-Works > *state* is zero the first time the function is called, allowing the > generator to perform any necessary initialization, and a positive > non-zero integer for each subsequent call. Usually the generator > function computes the list of possible completions when *state* is zero, > and returns them one at a time on subsequent calls. --- Lib/sqlite3/_completer.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py index a8351842d88394..7abefc777ed900 100644 --- a/Lib/sqlite3/_completer.py +++ b/Lib/sqlite3/_completer.py @@ -1,7 +1,7 @@ from contextlib import contextmanager -_keywords = ["ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", +_keywords = ("ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", @@ -25,13 +25,16 @@ "TABLE", "TEMP", "TEMPORARY", "THEN", "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", - "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT"] + "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT") +_completion_matches = [] def _complete(text, state): - options = [c + " " for c in _keywords if c.startswith(text.upper())] + global _completion_matches + if state == 0: + _completion_matches = [c + " " for c in _keywords if c.startswith(text.upper())] try: - return options[state] + return _completion_matches[state] except IndexError: return None From 311b4f3d81d237357dc7dce34a126b6c1527cc59 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 8 May 2025 06:35:02 +0800 Subject: [PATCH 10/36] Add whatsnew entry to 3.15 --- Doc/whatsnew/3.15.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 6ce7f964020fb9..647bee0487b4a3 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -70,6 +70,11 @@ Summary --- release highlights New features ============ +sqlite3 +------- + +* Support keyword completion in the :mod:`sqlite3` command-line interface. + (Contributed by Long Tan in :gh:`133393`.) Other language changes From 70f46e9781868924d63aadfe9a1dd3fd5815f5ee Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sat, 10 May 2025 23:20:06 +0800 Subject: [PATCH 11/36] =?UTF-8?q?Address=20B=C3=A9n=C3=A9dikt's=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Lib/sqlite3/_completer.py | 4 ++-- Lib/test/test_sqlite3/test_cli.py | 28 ++++++++++++++++++++++------ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py index 7abefc777ed900..39bb751e773111 100644 --- a/Lib/sqlite3/_completer.py +++ b/Lib/sqlite3/_completer.py @@ -1,7 +1,7 @@ from contextlib import contextmanager -_keywords = ("ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", +KEYWORDS = ("ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", @@ -32,7 +32,7 @@ def _complete(text, state): global _completion_matches if state == 0: - _completion_matches = [c + " " for c in _keywords if c.startswith(text.upper())] + _completion_matches = [c + " " for c in KEYWORDS if c.startswith(text.upper())] try: return _completion_matches[state] except IndexError: diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index abeb95e2897a1c..51a347919c07c2 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -4,6 +4,7 @@ import unittest from sqlite3.__main__ import main as cli +from sqlite3._completer import KEYWORDS from test.support.import_helper import import_module from test.support.os_helper import TESTFN, unlink from test.support.pty_helper import run_pty @@ -205,11 +206,9 @@ def test_color(self): '\x1b[35mnear "sel": syntax error\x1b[0m', err) @requires_subprocess() -class Completer(unittest.TestCase): +class CompletionTest(unittest.TestCase): @classmethod def setUpClass(cls): - # Ensure that the readline module is loaded - # If this fails, the test is skipped because SkipTest will be raised readline = import_module("readline") if readline.backend == "editline": raise unittest.SkipTest("libedit readline is not supported") @@ -217,10 +216,8 @@ def setUpClass(cls): def test_keyword_completion(self): script = "from sqlite3.__main__ import main; main()" # List candidates starting with 'S', there should be multiple matches. - # Then add 'EL' and complete 'SEL' to 'SELECT'. Quit console in the end - # to let run_pty() return. input = b"S\t\tEL\t 1;\n.quit\n" - output = run_pty(script, input) + output = run_pty(script, input, env={"NO_COLOR": "1"}) # Remove control sequences that colorize typed prefix 'S' output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output) self.assertIn(b"SELECT", output) @@ -235,5 +232,24 @@ def test_keyword_completion(self): self.assertIn(b"SELECT", output) self.assertIn(b"(1,)", output) + def test_nothing_to_complete(self): + script = "from sqlite3.__main__ import main; main()" + input = b"zzzz\t;\n.quit\n" + output = run_pty(script, input, env={"NO_COLOR": "1"}) + output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output) + for keyword in KEYWORDS: + self.assertNotRegex(output, rf"\b{keyword}\b".encode("utf-8")) + + def test_completion_order(self): + script = "from sqlite3.__main__ import main; main()" + input = b"S\t\n.quit\n" + output = run_pty(script, input, env={"NO_COLOR": "1"}) + output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output).strip() + savepoint_idx = output.find(b"SAVEPOINT") + select_idx = output.find(b"SELECT") + set_idx = output.find(b"SET") + self.assertTrue(0 <= savepoint_idx < select_idx < set_idx) + + if __name__ == "__main__": unittest.main() From 9d0373052b4da7f6c96f8c45984b22bab9e32fa5 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sat, 10 May 2025 23:22:13 +0800 Subject: [PATCH 12/36] Remove color handling of output; If CI fails might need to add back --- Lib/test/test_sqlite3/test_cli.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 51a347919c07c2..73b22a235d6069 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -218,8 +218,6 @@ def test_keyword_completion(self): # List candidates starting with 'S', there should be multiple matches. input = b"S\t\tEL\t 1;\n.quit\n" output = run_pty(script, input, env={"NO_COLOR": "1"}) - # Remove control sequences that colorize typed prefix 'S' - output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output) self.assertIn(b"SELECT", output) self.assertIn(b"SET", output) self.assertIn(b"SAVEPOINT", output) @@ -236,7 +234,6 @@ def test_nothing_to_complete(self): script = "from sqlite3.__main__ import main; main()" input = b"zzzz\t;\n.quit\n" output = run_pty(script, input, env={"NO_COLOR": "1"}) - output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output) for keyword in KEYWORDS: self.assertNotRegex(output, rf"\b{keyword}\b".encode("utf-8")) @@ -244,7 +241,6 @@ def test_completion_order(self): script = "from sqlite3.__main__ import main; main()" input = b"S\t\n.quit\n" output = run_pty(script, input, env={"NO_COLOR": "1"}) - output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output).strip() savepoint_idx = output.find(b"SAVEPOINT") select_idx = output.find(b"SELECT") set_idx = output.find(b"SET") From bfcff38883af1b3310282d6fbc1e326a8d425f32 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sat, 10 May 2025 23:50:40 +0800 Subject: [PATCH 13/36] Fix `run_pty()` doesn't return and test hangs --- 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 73b22a235d6069..a0392cd29eac42 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -239,7 +239,7 @@ def test_nothing_to_complete(self): def test_completion_order(self): script = "from sqlite3.__main__ import main; main()" - input = b"S\t\n.quit\n" + input = b"S\t;\n.quit\n" output = run_pty(script, input, env={"NO_COLOR": "1"}) savepoint_idx = output.find(b"SAVEPOINT") select_idx = output.find(b"SELECT") From 805d9971f243b8a5faa20553a97e08e5dfb0f579 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 11 May 2025 00:17:43 +0800 Subject: [PATCH 14/36] Revert "Remove color handling of output; If CI fails might need to add back" This reverts commit 9d0373052b4da7f6c96f8c45984b22bab9e32fa5. --- Lib/test/test_sqlite3/test_cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index a0392cd29eac42..a1e95495648c57 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -218,6 +218,8 @@ def test_keyword_completion(self): # List candidates starting with 'S', there should be multiple matches. input = b"S\t\tEL\t 1;\n.quit\n" output = run_pty(script, input, env={"NO_COLOR": "1"}) + # Remove control sequences that colorize typed prefix 'S' + output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output) self.assertIn(b"SELECT", output) self.assertIn(b"SET", output) self.assertIn(b"SAVEPOINT", output) @@ -234,6 +236,7 @@ def test_nothing_to_complete(self): script = "from sqlite3.__main__ import main; main()" input = b"zzzz\t;\n.quit\n" output = run_pty(script, input, env={"NO_COLOR": "1"}) + output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output) for keyword in KEYWORDS: self.assertNotRegex(output, rf"\b{keyword}\b".encode("utf-8")) @@ -241,6 +244,7 @@ def test_completion_order(self): script = "from sqlite3.__main__ import main; main()" input = b"S\t;\n.quit\n" output = run_pty(script, input, env={"NO_COLOR": "1"}) + output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output).strip() savepoint_idx = output.find(b"SAVEPOINT") select_idx = output.find(b"SELECT") set_idx = output.find(b"SET") From 276b4a75c61eca8102f37878d7193e31c9a7c563 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 11 May 2025 01:29:59 +0800 Subject: [PATCH 15/36] Turn off colored-completion-prefix for readline --- Lib/test/test_sqlite3/test_cli.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index a1e95495648c57..c4e524988eca42 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -1,6 +1,6 @@ """sqlite3 CLI tests.""" -import re import sqlite3 +import textwrap import unittest from sqlite3.__main__ import main as cli @@ -214,12 +214,14 @@ def setUpClass(cls): raise unittest.SkipTest("libedit readline is not supported") def test_keyword_completion(self): - script = "from sqlite3.__main__ import main; main()" + script = textwrap.dedent(""" + import readline + readline.parse_and_bind("set colored-completion-prefix off") + from sqlite3.__main__ import main; main() + """) # List candidates starting with 'S', there should be multiple matches. input = b"S\t\tEL\t 1;\n.quit\n" - output = run_pty(script, input, env={"NO_COLOR": "1"}) - # Remove control sequences that colorize typed prefix 'S' - output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output) + output = run_pty(script, input) self.assertIn(b"SELECT", output) self.assertIn(b"SET", output) self.assertIn(b"SAVEPOINT", output) @@ -228,23 +230,28 @@ def test_keyword_completion(self): # Keywords are completed in upper case for even lower case user input input = b"sel\t\t 1;\n.quit\n" output = run_pty(script, input) - output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output) self.assertIn(b"SELECT", output) self.assertIn(b"(1,)", output) def test_nothing_to_complete(self): - script = "from sqlite3.__main__ import main; main()" + script = textwrap.dedent(""" + import readline + readline.parse_and_bind("set colored-completion-prefix off") + from sqlite3.__main__ import main; main() + """) input = b"zzzz\t;\n.quit\n" output = run_pty(script, input, env={"NO_COLOR": "1"}) - output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output) for keyword in KEYWORDS: self.assertNotRegex(output, rf"\b{keyword}\b".encode("utf-8")) def test_completion_order(self): - script = "from sqlite3.__main__ import main; main()" + script = textwrap.dedent(""" + import readline + readline.parse_and_bind("set colored-completion-prefix off") + from sqlite3.__main__ import main; main() + """) input = b"S\t;\n.quit\n" output = run_pty(script, input, env={"NO_COLOR": "1"}) - output = re.sub(rb"\x1b\[[0-9;]*[mK]", b"", output).strip() savepoint_idx = output.find(b"SAVEPOINT") select_idx = output.find(b"SELECT") set_idx = output.find(b"SET") From 09eeac84580444debfb799bf1b5258bd594e3902 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 11 May 2025 01:45:28 +0800 Subject: [PATCH 16/36] No need to pass "NO_COLOR" to `run_pty()` "NO_COLOR" only affects color for the `sqlite>` prompt, it does not help for disabling color of completion prefix. --- 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 c4e524988eca42..a6f019b98f9460 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -240,7 +240,7 @@ def test_nothing_to_complete(self): from sqlite3.__main__ import main; main() """) input = b"zzzz\t;\n.quit\n" - output = run_pty(script, input, env={"NO_COLOR": "1"}) + output = run_pty(script, input) for keyword in KEYWORDS: self.assertNotRegex(output, rf"\b{keyword}\b".encode("utf-8")) @@ -251,7 +251,7 @@ def test_completion_order(self): from sqlite3.__main__ import main; main() """) input = b"S\t;\n.quit\n" - output = run_pty(script, input, env={"NO_COLOR": "1"}) + output = run_pty(script, input) savepoint_idx = output.find(b"SAVEPOINT") select_idx = output.find(b"SELECT") set_idx = output.find(b"SET") From fc57d7133249b8fd622f878bfcee1975179d8c74 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 11 May 2025 01:57:06 +0800 Subject: [PATCH 17/36] Flip name --- Doc/whatsnew/3.15.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 647bee0487b4a3..2a0c6400df92fc 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -74,7 +74,7 @@ sqlite3 ------- * Support keyword completion in the :mod:`sqlite3` command-line interface. - (Contributed by Long Tan in :gh:`133393`.) + (Contributed by Tan Long in :gh:`133393`.) Other language changes From c5080695d95c92fd05ff607eb63fbe9b30ce05ce Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 11 May 2025 03:40:02 +0800 Subject: [PATCH 18/36] Triggering completion on Ubuntu requires 2 tabs --- 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 a6f019b98f9460..8e978e30eaea6e 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -250,7 +250,7 @@ def test_completion_order(self): readline.parse_and_bind("set colored-completion-prefix off") from sqlite3.__main__ import main; main() """) - input = b"S\t;\n.quit\n" + input = b"S\t\t;\n.quit\n" output = run_pty(script, input) savepoint_idx = output.find(b"SAVEPOINT") select_idx = output.find(b"SELECT") From 231b9e7a6eb98cd52fd7a25775579cb8783ddad0 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 11 May 2025 05:52:57 +0800 Subject: [PATCH 19/36] Move KEYWORDS to C --- Lib/sqlite3/_completer.py | 31 ++----------------- Lib/test/test_sqlite3/test_cli.py | 4 +-- Modules/_sqlite/module.c | 50 +++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 30 deletions(-) diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py index 39bb751e773111..8be0ce3ab285b0 100644 --- a/Lib/sqlite3/_completer.py +++ b/Lib/sqlite3/_completer.py @@ -1,38 +1,13 @@ +from _sqlite3 import SQLITE_KEYWORDS from contextlib import contextmanager - -KEYWORDS = ("ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", - "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", - "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE", "CAST", - "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", - "CONSTRAINT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", - "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", - "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", - "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", - "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", - "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", - "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", - "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", - "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", - "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", - "MATCH", "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", - "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", "OR", - "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", - "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", - "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", - "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", - "ROLLBACK", "ROW", "ROWS", "SAVEPOINT", "SELECT", "SET", - "TABLE", "TEMP", "TEMPORARY", "THEN", "TIES", "TO", - "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", - "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", - "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT") - _completion_matches = [] + def _complete(text, state): global _completion_matches if state == 0: - _completion_matches = [c + " " for c in KEYWORDS if c.startswith(text.upper())] + _completion_matches = [c + " " for c in SQLITE_KEYWORDS if c.startswith(text.upper())] try: return _completion_matches[state] except IndexError: diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 8e978e30eaea6e..8c712eb6240261 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -3,8 +3,8 @@ import textwrap import unittest +from _sqlite3 import SQLITE_KEYWORDS from sqlite3.__main__ import main as cli -from sqlite3._completer import KEYWORDS from test.support.import_helper import import_module from test.support.os_helper import TESTFN, unlink from test.support.pty_helper import run_pty @@ -241,7 +241,7 @@ def test_nothing_to_complete(self): """) input = b"zzzz\t;\n.quit\n" output = run_pty(script, input) - for keyword in KEYWORDS: + for keyword in SQLITE_KEYWORDS: self.assertNotRegex(output, rf"\b{keyword}\b".encode("utf-8")) def test_completion_order(self): diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 909ddd1f990e19..0fe0a757b2d322 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -404,6 +404,51 @@ pysqlite_error_name(int rc) return NULL; } +static int +add_sequence_constants(PyObject *module) { + PyObject *kwd; + const char *_keywords[] = { + "ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", + "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", + "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", + "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", "CURRENT", + "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", + "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT", "DO", + "DROP", "EACH", "ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUDE", "EXCLUSIVE", + "EXISTS", "EXPLAIN", "FAIL", "FILTER", "FIRST", "FOLLOWING", "FOR", + "FOREIGN", "FROM", "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", + "HAVING", "IF", "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", + "INITIALLY", "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", + "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", "MATCH", + "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", "NOTNULL", "NULL", + "NULLS", "OF", "OFFSET", "ON", "OR", "ORDER", "OTHERS", "OUTER", "OVER", + "PARTITION", "PLAN", "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", + "RANGE", "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", + "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", "ROLLBACK", "ROW", + "ROWS", "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", + "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", + "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", "WHERE", + "WINDOW", "WITH", "WITHOUT", NULL + }; + PyObject *keywords = PyTuple_New(147); + if (keywords == NULL) { + return -1; + } + for (int i = 0; _keywords[i] != NULL; i++) { + kwd = PyUnicode_FromString(_keywords[i]); + if (PyTuple_SetItem(keywords, i, kwd) != 0) { + Py_DECREF(kwd); + Py_DECREF(keywords); + return -1; + } + } + if (PyModule_Add(module, "SQLITE_KEYWORDS", keywords) < 0) { + Py_DECREF(keywords); + return -1; + } + return 0; +} + static int add_integer_constants(PyObject *module) { #define ADD_INT(ival) \ @@ -702,6 +747,11 @@ module_exec(PyObject *module) goto error; } + /* Set sequence constants */ + if (add_sequence_constants(module) < 0) { + goto error; + } + if (PyModule_AddStringConstant(module, "sqlite_version", sqlite3_libversion())) { goto error; } From 121b06913d9131347fe11231a4d5ff83c06e8dc4 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 11 May 2025 06:18:31 +0800 Subject: [PATCH 20/36] Improve style of C code 4-space indents; 79 line width; outermost curly braces in column 1 in function definition; blank line after local variable declaration; --- Modules/_sqlite/module.c | 87 +++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 0fe0a757b2d322..ecc9564363f240 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -405,48 +405,51 @@ pysqlite_error_name(int rc) } static int -add_sequence_constants(PyObject *module) { - PyObject *kwd; - const char *_keywords[] = { - "ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", - "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", - "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", - "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", "CURRENT", - "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", - "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT", "DO", - "DROP", "EACH", "ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUDE", "EXCLUSIVE", - "EXISTS", "EXPLAIN", "FAIL", "FILTER", "FIRST", "FOLLOWING", "FOR", - "FOREIGN", "FROM", "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", - "HAVING", "IF", "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", - "INITIALLY", "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", - "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", "MATCH", - "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", "NOTNULL", "NULL", - "NULLS", "OF", "OFFSET", "ON", "OR", "ORDER", "OTHERS", "OUTER", "OVER", - "PARTITION", "PLAN", "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", - "RANGE", "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", - "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", "ROLLBACK", "ROW", - "ROWS", "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", - "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", - "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", "WHERE", - "WINDOW", "WITH", "WITHOUT", NULL - }; - PyObject *keywords = PyTuple_New(147); - if (keywords == NULL) { - return -1; - } - for (int i = 0; _keywords[i] != NULL; i++) { - kwd = PyUnicode_FromString(_keywords[i]); - if (PyTuple_SetItem(keywords, i, kwd) != 0) { - Py_DECREF(kwd); - Py_DECREF(keywords); - return -1; - } - } - if (PyModule_Add(module, "SQLITE_KEYWORDS", keywords) < 0) { - Py_DECREF(keywords); - return -1; - } - return 0; +add_sequence_constants(PyObject *module) +{ + PyObject *kwd; + const char *_keywords[] = { + "ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", + "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", + "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", + "COLUMN", "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", + "CURRENT", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", + "DATABASE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DELETE", "DESC", + "DETACH", "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", + "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", + "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", "FULL", + "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", "IGNORE", + "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", "INNER", "INSERT", + "INSTEAD", "INTERSECT", "INTO", "IS", "ISNULL", "JOIN", "KEY", "LAST", + "LEFT", "LIKE", "LIMIT", "MATCH", "MATERIALIZED", "NATURAL", "NO", + "NOT", "NOTHING", "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", + "OR", "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", + "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", + "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", "RENAME", + "REPLACE", "RESTRICT", "RETURNING", "RIGHT", "ROLLBACK", "ROW", "ROWS", + "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", + "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", + "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", + "WHERE", "WINDOW", "WITH", "WITHOUT", NULL + }; + PyObject *keywords = PyTuple_New(147); + + if (keywords == NULL) { + return -1; + } + for (int i = 0; _keywords[i] != NULL; i++) { + kwd = PyUnicode_FromString(_keywords[i]); + if (PyTuple_SetItem(keywords, i, kwd) != 0) { + Py_DECREF(kwd); + Py_DECREF(keywords); + return -1; + } + } + if (PyModule_Add(module, "SQLITE_KEYWORDS", keywords) < 0) { + Py_DECREF(keywords); + return -1; + } + return 0; } static int From 90a86cfd01c85c94e34d3a36bbab84f28eb0653c Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 11 May 2025 17:21:43 +0800 Subject: [PATCH 21/36] Improve tests --- Lib/test/test_sqlite3/test_cli.py | 38 ++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 8c712eb6240261..e4dfa2bc2df63f 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -207,6 +207,8 @@ def test_color(self): @requires_subprocess() class CompletionTest(unittest.TestCase): + PS1 = "sqlite> " + @classmethod def setUpClass(cls): readline = import_module("readline") @@ -239,24 +241,38 @@ def test_nothing_to_complete(self): readline.parse_and_bind("set colored-completion-prefix off") from sqlite3.__main__ import main; main() """) - input = b"zzzz\t;\n.quit\n" - output = run_pty(script, input) - for keyword in SQLITE_KEYWORDS: - self.assertNotRegex(output, rf"\b{keyword}\b".encode("utf-8")) + input = b"xyzzy\t\t\b\b\b\b\b.quit\n" + output = run_pty(script, input, env={"NO_COLOR": "1"}) + output_lines = output.decode().splitlines() + line_num = next(i for i, line in enumerate(output_lines, 1) + if line.startswith(f"{self.PS1}xyzzy")) + # completions occupy lines, assert no extra lines when there is nothing + # to complete + self.assertEqual(line_num, len(output_lines)) def test_completion_order(self): script = textwrap.dedent(""" import readline readline.parse_and_bind("set colored-completion-prefix off") + # hide control sequences surrounding each candidate + readline.parse_and_bind("set colored-stats off") + # hide "Display all xxx possibilities? (y or n)" + readline.parse_and_bind("set completion-query-items 0") + # hide "--More--" + readline.parse_and_bind("set page-completions off") + # show candidates one per line + readline.parse_and_bind("set completion-display-width 0") from sqlite3.__main__ import main; main() """) - input = b"S\t\t;\n.quit\n" - output = run_pty(script, input) - savepoint_idx = output.find(b"SAVEPOINT") - select_idx = output.find(b"SELECT") - set_idx = output.find(b"SET") - self.assertTrue(0 <= savepoint_idx < select_idx < set_idx) - + input = b"\t\t.quit\n" + output = run_pty(script, input, env={"NO_COLOR": "1"}) + output_lines = output.decode().splitlines() + candidates = [] + for line in output_lines[-2::-1]: + if line.startswith(self.PS1): + break + candidates.append(line.strip()) + self.assertEqual(sorted(candidates, reverse=True), candidates) if __name__ == "__main__": unittest.main() From 51707337ce5a2d44f28deb70a8198a1155d6b0c4 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 16 May 2025 13:08:06 +0800 Subject: [PATCH 22/36] =?UTF-8?q?Address=20B=C3=A9n=C3=A9dikt's=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Doc/whatsnew/3.15.rst | 13 +++++++------ Lib/sqlite3/_completer.py | 5 +++-- Lib/test/test_sqlite3/test_cli.py | 24 +++++++++++++----------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 2a0c6400df92fc..fea11608a0230b 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -70,12 +70,6 @@ Summary --- release highlights New features ============ -sqlite3 -------- - -* Support keyword completion in the :mod:`sqlite3` command-line interface. - (Contributed by Tan Long in :gh:`133393`.) - Other language changes ====================== @@ -94,6 +88,13 @@ New modules Improved modules ================ +sqlite3 +------- + +* Support keyword completion in the :mod:`sqlite3` command-line interface. + (Contributed by Tan Long in :gh:`133393`.) + + ssl --- diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py index 8be0ce3ab285b0..fd18473fbcb815 100644 --- a/Lib/sqlite3/_completer.py +++ b/Lib/sqlite3/_completer.py @@ -7,9 +7,10 @@ def _complete(text, state): global _completion_matches if state == 0: - _completion_matches = [c + " " for c in SQLITE_KEYWORDS if c.startswith(text.upper())] + text_upper = text.upper() + _completion_matches = [c for c in SQLITE_KEYWORDS if c.startswith(text_upper)] try: - return _completion_matches[state] + return _completion_matches[state] + " " except IndexError: return None diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index e4dfa2bc2df63f..006986df4c2975 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -205,6 +205,7 @@ def test_color(self): self.assertIn('\x1b[1;35mOperationalError (SQLITE_ERROR)\x1b[0m: ' '\x1b[35mnear "sel": syntax error\x1b[0m', err) + @requires_subprocess() class CompletionTest(unittest.TestCase): PS1 = "sqlite> " @@ -215,15 +216,18 @@ def setUpClass(cls): if readline.backend == "editline": raise unittest.SkipTest("libedit readline is not supported") - def test_keyword_completion(self): + def write_input(self, input, env=None): script = textwrap.dedent(""" import readline readline.parse_and_bind("set colored-completion-prefix off") from sqlite3.__main__ import main; main() """) + return run_pty(script, input, env) + + def test_keyword_completion(self): # List candidates starting with 'S', there should be multiple matches. input = b"S\t\tEL\t 1;\n.quit\n" - output = run_pty(script, input) + output = self.write_input(input) self.assertIn(b"SELECT", output) self.assertIn(b"SET", output) self.assertIn(b"SAVEPOINT", output) @@ -231,21 +235,18 @@ def test_keyword_completion(self): # Keywords are completed in upper case for even lower case user input input = b"sel\t\t 1;\n.quit\n" - output = run_pty(script, input) + output = self.write_input(input) self.assertIn(b"SELECT", output) self.assertIn(b"(1,)", output) def test_nothing_to_complete(self): - script = textwrap.dedent(""" - import readline - readline.parse_and_bind("set colored-completion-prefix off") - from sqlite3.__main__ import main; main() - """) input = b"xyzzy\t\t\b\b\b\b\b.quit\n" - output = run_pty(script, input, env={"NO_COLOR": "1"}) + # set NO_COLOR to disable coloring for self.PS1 + output = self.write_input(input, env={"NO_COLOR": "1"}) output_lines = output.decode().splitlines() - line_num = next(i for i, line in enumerate(output_lines, 1) - if line.startswith(f"{self.PS1}xyzzy")) + line_num = next((i for i, line in enumerate(output_lines, 1) + if line.startswith(f"{self.PS1}xyzzy")), -1) + self.assertNotEqual(line_num, -1) # completions occupy lines, assert no extra lines when there is nothing # to complete self.assertEqual(line_num, len(output_lines)) @@ -274,5 +275,6 @@ def test_completion_order(self): candidates.append(line.strip()) self.assertEqual(sorted(candidates, reverse=True), candidates) + if __name__ == "__main__": unittest.main() From b40982ab311335380fea7e42fb4eef7d4d949e74 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 16 May 2025 13:08:57 +0800 Subject: [PATCH 23/36] Revert "Improve style of C code" This reverts commit 121b06913d9131347fe11231a4d5ff83c06e8dc4. --- Modules/_sqlite/module.c | 87 +++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index ecc9564363f240..0fe0a757b2d322 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -405,51 +405,48 @@ pysqlite_error_name(int rc) } static int -add_sequence_constants(PyObject *module) -{ - PyObject *kwd; - const char *_keywords[] = { - "ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", - "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", - "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", - "COLUMN", "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", - "CURRENT", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", - "DATABASE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DELETE", "DESC", - "DETACH", "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", - "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", - "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", "FULL", - "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", "IGNORE", - "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", "INNER", "INSERT", - "INSTEAD", "INTERSECT", "INTO", "IS", "ISNULL", "JOIN", "KEY", "LAST", - "LEFT", "LIKE", "LIMIT", "MATCH", "MATERIALIZED", "NATURAL", "NO", - "NOT", "NOTHING", "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", - "OR", "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", - "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", - "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", "RENAME", - "REPLACE", "RESTRICT", "RETURNING", "RIGHT", "ROLLBACK", "ROW", "ROWS", - "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", - "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", - "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", - "WHERE", "WINDOW", "WITH", "WITHOUT", NULL - }; - PyObject *keywords = PyTuple_New(147); - - if (keywords == NULL) { - return -1; - } - for (int i = 0; _keywords[i] != NULL; i++) { - kwd = PyUnicode_FromString(_keywords[i]); - if (PyTuple_SetItem(keywords, i, kwd) != 0) { - Py_DECREF(kwd); - Py_DECREF(keywords); - return -1; - } - } - if (PyModule_Add(module, "SQLITE_KEYWORDS", keywords) < 0) { - Py_DECREF(keywords); - return -1; - } - return 0; +add_sequence_constants(PyObject *module) { + PyObject *kwd; + const char *_keywords[] = { + "ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", + "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", + "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", + "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", "CURRENT", + "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", + "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT", "DO", + "DROP", "EACH", "ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUDE", "EXCLUSIVE", + "EXISTS", "EXPLAIN", "FAIL", "FILTER", "FIRST", "FOLLOWING", "FOR", + "FOREIGN", "FROM", "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", + "HAVING", "IF", "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", + "INITIALLY", "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", + "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", "MATCH", + "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", "NOTNULL", "NULL", + "NULLS", "OF", "OFFSET", "ON", "OR", "ORDER", "OTHERS", "OUTER", "OVER", + "PARTITION", "PLAN", "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", + "RANGE", "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", + "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", "ROLLBACK", "ROW", + "ROWS", "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", + "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", + "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", "WHERE", + "WINDOW", "WITH", "WITHOUT", NULL + }; + PyObject *keywords = PyTuple_New(147); + if (keywords == NULL) { + return -1; + } + for (int i = 0; _keywords[i] != NULL; i++) { + kwd = PyUnicode_FromString(_keywords[i]); + if (PyTuple_SetItem(keywords, i, kwd) != 0) { + Py_DECREF(kwd); + Py_DECREF(keywords); + return -1; + } + } + if (PyModule_Add(module, "SQLITE_KEYWORDS", keywords) < 0) { + Py_DECREF(keywords); + return -1; + } + return 0; } static int From 226ea9fba9029b3c29092fae40040f5b4632eba5 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 16 May 2025 13:11:04 +0800 Subject: [PATCH 24/36] Revert "Move KEYWORDS to C" This reverts commit 231b9e7a6eb98cd52fd7a25775579cb8783ddad0. --- Lib/sqlite3/_completer.py | 31 +++++++++++++++++-- Lib/test/test_sqlite3/test_cli.py | 2 +- Modules/_sqlite/module.c | 50 ------------------------------- 3 files changed, 29 insertions(+), 54 deletions(-) diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py index fd18473fbcb815..58d991d7e590b3 100644 --- a/Lib/sqlite3/_completer.py +++ b/Lib/sqlite3/_completer.py @@ -1,14 +1,39 @@ -from _sqlite3 import SQLITE_KEYWORDS from contextlib import contextmanager -_completion_matches = [] +KEYWORDS = ("ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", + "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", + "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE", "CAST", + "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", + "CONSTRAINT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", + "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", + "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", + "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", + "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", + "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", + "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", + "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", + "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", + "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", + "MATCH", "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", + "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", "OR", + "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", + "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", + "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", + "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", + "ROLLBACK", "ROW", "ROWS", "SAVEPOINT", "SELECT", "SET", + "TABLE", "TEMP", "TEMPORARY", "THEN", "TIES", "TO", + "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", + "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", + "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT") + +_completion_matches = [] def _complete(text, state): global _completion_matches if state == 0: text_upper = text.upper() - _completion_matches = [c for c in SQLITE_KEYWORDS if c.startswith(text_upper)] + _completion_matches = [c for c in KEYWORDS if c.startswith(text_upper)] try: return _completion_matches[state] + " " except IndexError: diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 006986df4c2975..02fd6cc960fb72 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -3,8 +3,8 @@ import textwrap import unittest -from _sqlite3 import SQLITE_KEYWORDS from sqlite3.__main__ import main as cli +from sqlite3._completer import KEYWORDS from test.support.import_helper import import_module from test.support.os_helper import TESTFN, unlink from test.support.pty_helper import run_pty diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 0fe0a757b2d322..909ddd1f990e19 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -404,51 +404,6 @@ pysqlite_error_name(int rc) return NULL; } -static int -add_sequence_constants(PyObject *module) { - PyObject *kwd; - const char *_keywords[] = { - "ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", - "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", - "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", - "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", "CURRENT", - "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", - "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT", "DO", - "DROP", "EACH", "ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUDE", "EXCLUSIVE", - "EXISTS", "EXPLAIN", "FAIL", "FILTER", "FIRST", "FOLLOWING", "FOR", - "FOREIGN", "FROM", "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", - "HAVING", "IF", "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", - "INITIALLY", "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", - "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", "MATCH", - "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", "NOTNULL", "NULL", - "NULLS", "OF", "OFFSET", "ON", "OR", "ORDER", "OTHERS", "OUTER", "OVER", - "PARTITION", "PLAN", "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", - "RANGE", "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", - "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", "ROLLBACK", "ROW", - "ROWS", "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", - "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", - "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", "WHERE", - "WINDOW", "WITH", "WITHOUT", NULL - }; - PyObject *keywords = PyTuple_New(147); - if (keywords == NULL) { - return -1; - } - for (int i = 0; _keywords[i] != NULL; i++) { - kwd = PyUnicode_FromString(_keywords[i]); - if (PyTuple_SetItem(keywords, i, kwd) != 0) { - Py_DECREF(kwd); - Py_DECREF(keywords); - return -1; - } - } - if (PyModule_Add(module, "SQLITE_KEYWORDS", keywords) < 0) { - Py_DECREF(keywords); - return -1; - } - return 0; -} - static int add_integer_constants(PyObject *module) { #define ADD_INT(ival) \ @@ -747,11 +702,6 @@ module_exec(PyObject *module) goto error; } - /* Set sequence constants */ - if (add_sequence_constants(module) < 0) { - goto error; - } - if (PyModule_AddStringConstant(module, "sqlite_version", sqlite3_libversion())) { goto error; } From 4eebbd9aab095b370d203ceaa29b407e626b3acc Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 16 May 2025 07:28:26 +0200 Subject: [PATCH 25/36] Read keyword names dynamically --- Modules/_sqlite/module.c | 65 +++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index ecc9564363f240..e8c3bdfd69c18a 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -32,6 +32,7 @@ #include "microprotocols.h" #include "row.h" #include "blob.h" +#include "util.h" #if SQLITE_VERSION_NUMBER < 3015002 #error "SQLite 3.15.2 or higher required" @@ -405,51 +406,39 @@ pysqlite_error_name(int rc) } static int -add_sequence_constants(PyObject *module) +add_keyword_tuple(PyObject *module) { - PyObject *kwd; - const char *_keywords[] = { - "ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", - "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", - "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", - "COLUMN", "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", - "CURRENT", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", - "DATABASE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DELETE", "DESC", - "DETACH", "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", - "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", - "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", "FULL", - "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", "IGNORE", - "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", "INNER", "INSERT", - "INSTEAD", "INTERSECT", "INTO", "IS", "ISNULL", "JOIN", "KEY", "LAST", - "LEFT", "LIKE", "LIMIT", "MATCH", "MATERIALIZED", "NATURAL", "NO", - "NOT", "NOTHING", "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", - "OR", "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", - "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", - "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", "RENAME", - "REPLACE", "RESTRICT", "RETURNING", "RIGHT", "ROLLBACK", "ROW", "ROWS", - "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", - "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", - "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", - "WHERE", "WINDOW", "WITH", "WITHOUT", NULL - }; - PyObject *keywords = PyTuple_New(147); - + int count = sqlite3_keyword_count(); + PyObject *keywords = PyTuple_New(count); if (keywords == NULL) { - return -1; + goto error; } - for (int i = 0; _keywords[i] != NULL; i++) { - kwd = PyUnicode_FromString(_keywords[i]); - if (PyTuple_SetItem(keywords, i, kwd) != 0) { + for (int i = 0; i < count; i++) { + const char *keyword; + int size; + int result = sqlite3_keyword_name(i, &keyword, &size); + if (result != SQLITE_OK) { + pysqlite_state *state = pysqlite_get_state(module); + set_error_from_code(state, result); + goto error; + } + PyObject *kwd = PyUnicode_FromStringAndSize(keyword, size); + if (!kwd) { + goto error; + } + if (PyTuple_SetItem(keywords, i, kwd) < 0) { Py_DECREF(kwd); - Py_DECREF(keywords); - return -1; + goto error; } } if (PyModule_Add(module, "SQLITE_KEYWORDS", keywords) < 0) { - Py_DECREF(keywords); - return -1; + goto error; } return 0; + +error: + Py_XDECREF(keywords); + return -1; } static int @@ -750,8 +739,8 @@ module_exec(PyObject *module) goto error; } - /* Set sequence constants */ - if (add_sequence_constants(module) < 0) { + /* Set the keyword tuple */ + if (add_keyword_tuple(module) < 0) { goto error; } From 3f9b2c1aca5625526ac346b2e447c2a182c25971 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 16 May 2025 16:16:04 +0800 Subject: [PATCH 26/36] Check candidates against KEYWORDS --- 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 02fd6cc960fb72..2e75681c4c3fed 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -273,7 +273,7 @@ def test_completion_order(self): if line.startswith(self.PS1): break candidates.append(line.strip()) - self.assertEqual(sorted(candidates, reverse=True), candidates) + self.assertEqual(sorted(candidates), list(KEYWORDS)) if __name__ == "__main__": From 0410fa272bf56fd360cc51b8cb128db6a50cdd24 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 16 May 2025 16:37:24 +0800 Subject: [PATCH 27/36] Use slice to get candidates --- Doc/whatsnew/3.15.rst | 3 ++- Lib/test/test_sqlite3/test_cli.py | 9 ++++----- Misc/ACKS | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index fea11608a0230b..d7ea0bdf9ad302 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -71,6 +71,7 @@ New features ============ + Other language changes ====================== @@ -92,7 +93,7 @@ sqlite3 ------- * Support keyword completion in the :mod:`sqlite3` command-line interface. - (Contributed by Tan Long in :gh:`133393`.) + (Contributed by Long Tan in :gh:`133393`.) ssl diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 2e75681c4c3fed..6273655366e08f 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -268,11 +268,10 @@ def test_completion_order(self): input = b"\t\t.quit\n" output = run_pty(script, input, env={"NO_COLOR": "1"}) output_lines = output.decode().splitlines() - candidates = [] - for line in output_lines[-2::-1]: - if line.startswith(self.PS1): - break - candidates.append(line.strip()) + slices = tuple(i for i, line in enumerate(output_lines) if line.startswith(self.PS1)) + self.assertEqual(len(slices), 2) + start, end = slices + candidates = [c.strip() for c in output_lines[start+1 : end]] self.assertEqual(sorted(candidates), list(KEYWORDS)) diff --git a/Misc/ACKS b/Misc/ACKS index 610dcf9f4238de..8574e59cd6eee9 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1864,6 +1864,7 @@ Neil Tallim Geoff Talvola Anish Tambe Musashi Tamura +Long Tan William Tanksley Christian Tanzer Steven Taschuk From bd0b9ce432b53345f92297164a7937a9deea6362 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 16 May 2025 17:00:19 +0800 Subject: [PATCH 28/36] =?UTF-8?q?Address=20B=C3=A9n=C3=A9dikt's=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Lib/test/test_sqlite3/test_cli.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 6273655366e08f..77048d7b51267f 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -268,11 +268,12 @@ def test_completion_order(self): input = b"\t\t.quit\n" output = run_pty(script, input, env={"NO_COLOR": "1"}) output_lines = output.decode().splitlines() - slices = tuple(i for i, line in enumerate(output_lines) if line.startswith(self.PS1)) - self.assertEqual(len(slices), 2) - start, end = slices - candidates = [c.strip() for c in output_lines[start+1 : end]] - self.assertEqual(sorted(candidates), list(KEYWORDS)) + indices = [i for i, line in enumerate(output_lines) + if line.startswith(self.PS1)] + self.assertEqual(len(indices), 2) + start, end = indices[0] + 1, indices[1] + candidates = list(map(str.strip, output_lines[start:end])) + self.assertEqual(candidates, list(KEYWORDS)) if __name__ == "__main__": From 35a17e7b0d03acb02ba34775a05b5e2b6ce8c9ee Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 16 May 2025 17:03:55 +0800 Subject: [PATCH 29/36] Make candidates tuple --- 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 77048d7b51267f..1b462972a4601c 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -272,8 +272,8 @@ def test_completion_order(self): if line.startswith(self.PS1)] self.assertEqual(len(indices), 2) start, end = indices[0] + 1, indices[1] - candidates = list(map(str.strip, output_lines[start:end])) - self.assertEqual(candidates, list(KEYWORDS)) + candidates = tuple(map(str.strip, output_lines[start:end])) + self.assertEqual(candidates, KEYWORDS) if __name__ == "__main__": From 3dd16b37c9f819bffd0aa026118d48de00286093 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 16 May 2025 17:43:38 +0800 Subject: [PATCH 30/36] Revert "Revert "Move KEYWORDS to C"" This reverts commit 226ea9fba9029b3c29092fae40040f5b4632eba5. --- Lib/sqlite3/_completer.py | 31 ++----------------- Lib/test/test_sqlite3/test_cli.py | 2 +- Modules/_sqlite/module.c | 50 +++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 29 deletions(-) diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py index 58d991d7e590b3..fd18473fbcb815 100644 --- a/Lib/sqlite3/_completer.py +++ b/Lib/sqlite3/_completer.py @@ -1,39 +1,14 @@ +from _sqlite3 import SQLITE_KEYWORDS from contextlib import contextmanager - -KEYWORDS = ("ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", - "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", - "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE", "CAST", - "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", - "CONSTRAINT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", - "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", - "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", - "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", - "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", - "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", - "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", - "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", - "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", - "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", - "MATCH", "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", - "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", "OR", - "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", - "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", - "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", - "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", - "ROLLBACK", "ROW", "ROWS", "SAVEPOINT", "SELECT", "SET", - "TABLE", "TEMP", "TEMPORARY", "THEN", "TIES", "TO", - "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", - "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", - "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT") - _completion_matches = [] + def _complete(text, state): global _completion_matches if state == 0: text_upper = text.upper() - _completion_matches = [c for c in KEYWORDS if c.startswith(text_upper)] + _completion_matches = [c for c in SQLITE_KEYWORDS if c.startswith(text_upper)] try: return _completion_matches[state] + " " except IndexError: diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 1b462972a4601c..61dc39d67f0cec 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -3,8 +3,8 @@ import textwrap import unittest +from _sqlite3 import SQLITE_KEYWORDS from sqlite3.__main__ import main as cli -from sqlite3._completer import KEYWORDS from test.support.import_helper import import_module from test.support.os_helper import TESTFN, unlink from test.support.pty_helper import run_pty diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 909ddd1f990e19..0fe0a757b2d322 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -404,6 +404,51 @@ pysqlite_error_name(int rc) return NULL; } +static int +add_sequence_constants(PyObject *module) { + PyObject *kwd; + const char *_keywords[] = { + "ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", + "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", + "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", + "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", "CURRENT", + "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", + "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT", "DO", + "DROP", "EACH", "ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUDE", "EXCLUSIVE", + "EXISTS", "EXPLAIN", "FAIL", "FILTER", "FIRST", "FOLLOWING", "FOR", + "FOREIGN", "FROM", "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", + "HAVING", "IF", "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", + "INITIALLY", "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", + "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", "MATCH", + "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", "NOTNULL", "NULL", + "NULLS", "OF", "OFFSET", "ON", "OR", "ORDER", "OTHERS", "OUTER", "OVER", + "PARTITION", "PLAN", "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", + "RANGE", "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", + "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", "ROLLBACK", "ROW", + "ROWS", "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", + "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", + "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", "WHERE", + "WINDOW", "WITH", "WITHOUT", NULL + }; + PyObject *keywords = PyTuple_New(147); + if (keywords == NULL) { + return -1; + } + for (int i = 0; _keywords[i] != NULL; i++) { + kwd = PyUnicode_FromString(_keywords[i]); + if (PyTuple_SetItem(keywords, i, kwd) != 0) { + Py_DECREF(kwd); + Py_DECREF(keywords); + return -1; + } + } + if (PyModule_Add(module, "SQLITE_KEYWORDS", keywords) < 0) { + Py_DECREF(keywords); + return -1; + } + return 0; +} + static int add_integer_constants(PyObject *module) { #define ADD_INT(ival) \ @@ -702,6 +747,11 @@ module_exec(PyObject *module) goto error; } + /* Set sequence constants */ + if (add_sequence_constants(module) < 0) { + goto error; + } + if (PyModule_AddStringConstant(module, "sqlite_version", sqlite3_libversion())) { goto error; } From f3ea951f1c67ffd277933b5504576a5896383dc4 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 16 May 2025 17:43:45 +0800 Subject: [PATCH 31/36] Revert "Revert "Improve style of C code"" This reverts commit b40982ab311335380fea7e42fb4eef7d4d949e74. --- Modules/_sqlite/module.c | 87 +++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 0fe0a757b2d322..ecc9564363f240 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -405,48 +405,51 @@ pysqlite_error_name(int rc) } static int -add_sequence_constants(PyObject *module) { - PyObject *kwd; - const char *_keywords[] = { - "ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", - "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", - "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", - "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", "CURRENT", - "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", - "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT", "DO", - "DROP", "EACH", "ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUDE", "EXCLUSIVE", - "EXISTS", "EXPLAIN", "FAIL", "FILTER", "FIRST", "FOLLOWING", "FOR", - "FOREIGN", "FROM", "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", - "HAVING", "IF", "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", - "INITIALLY", "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", - "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", "MATCH", - "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", "NOTNULL", "NULL", - "NULLS", "OF", "OFFSET", "ON", "OR", "ORDER", "OTHERS", "OUTER", "OVER", - "PARTITION", "PLAN", "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", - "RANGE", "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", - "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", "ROLLBACK", "ROW", - "ROWS", "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", - "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", - "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", "WHERE", - "WINDOW", "WITH", "WITHOUT", NULL - }; - PyObject *keywords = PyTuple_New(147); - if (keywords == NULL) { - return -1; - } - for (int i = 0; _keywords[i] != NULL; i++) { - kwd = PyUnicode_FromString(_keywords[i]); - if (PyTuple_SetItem(keywords, i, kwd) != 0) { - Py_DECREF(kwd); - Py_DECREF(keywords); - return -1; - } - } - if (PyModule_Add(module, "SQLITE_KEYWORDS", keywords) < 0) { - Py_DECREF(keywords); - return -1; - } - return 0; +add_sequence_constants(PyObject *module) +{ + PyObject *kwd; + const char *_keywords[] = { + "ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", + "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", + "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", + "COLUMN", "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", + "CURRENT", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", + "DATABASE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DELETE", "DESC", + "DETACH", "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", + "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", + "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", "FULL", + "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", "IGNORE", + "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", "INNER", "INSERT", + "INSTEAD", "INTERSECT", "INTO", "IS", "ISNULL", "JOIN", "KEY", "LAST", + "LEFT", "LIKE", "LIMIT", "MATCH", "MATERIALIZED", "NATURAL", "NO", + "NOT", "NOTHING", "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", + "OR", "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", + "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", + "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", "RENAME", + "REPLACE", "RESTRICT", "RETURNING", "RIGHT", "ROLLBACK", "ROW", "ROWS", + "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", + "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", + "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", + "WHERE", "WINDOW", "WITH", "WITHOUT", NULL + }; + PyObject *keywords = PyTuple_New(147); + + if (keywords == NULL) { + return -1; + } + for (int i = 0; _keywords[i] != NULL; i++) { + kwd = PyUnicode_FromString(_keywords[i]); + if (PyTuple_SetItem(keywords, i, kwd) != 0) { + Py_DECREF(kwd); + Py_DECREF(keywords); + return -1; + } + } + if (PyModule_Add(module, "SQLITE_KEYWORDS", keywords) < 0) { + Py_DECREF(keywords); + return -1; + } + return 0; } static int From 34cfc785db234b811eabd4b75e7f0a2de73e9458 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 16 May 2025 17:48:34 +0800 Subject: [PATCH 32/36] Fix 'KEYWORDS' not found --- 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 61dc39d67f0cec..74e40186ddd6db 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -273,7 +273,7 @@ def test_completion_order(self): self.assertEqual(len(indices), 2) start, end = indices[0] + 1, indices[1] candidates = tuple(map(str.strip, output_lines[start:end])) - self.assertEqual(candidates, KEYWORDS) + self.assertEqual(candidates, SQLITE_KEYWORDS) if __name__ == "__main__": From 477b48b2ec15d2596f53a6ed6ec09a581e7a6b15 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 16 May 2025 18:29:24 +0800 Subject: [PATCH 33/36] Sort keywords before checking the equality GNU Readline always sorts completions before displaying them. There is a [rl_sort_completion_matches](https://git.savannah.gnu.org/cgit/readline.git/tree/complete.c#n411) in Readline's source code but it's not exposed as a config flag. --- 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 74e40186ddd6db..5f80890ceda6b2 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -273,7 +273,7 @@ def test_completion_order(self): self.assertEqual(len(indices), 2) start, end = indices[0] + 1, indices[1] candidates = tuple(map(str.strip, output_lines[start:end])) - self.assertEqual(candidates, SQLITE_KEYWORDS) + self.assertEqual(candidates, sorted(SQLITE_KEYWORDS)) if __name__ == "__main__": From 68bb4f39d40479a3b1ab84ffceb0b72168faeee5 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 16 May 2025 18:33:18 +0800 Subject: [PATCH 34/36] Fix comparing between tuple and list From 4c3b122eabc35cb7c4d2c055dfc47935889b83e5 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 16 May 2025 18:41:15 +0800 Subject: [PATCH 35/36] Fix comparing between tuple and list --- 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 5f80890ceda6b2..613d165e596081 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -272,7 +272,7 @@ def test_completion_order(self): if line.startswith(self.PS1)] self.assertEqual(len(indices), 2) start, end = indices[0] + 1, indices[1] - candidates = tuple(map(str.strip, output_lines[start:end])) + candidates = list(map(str.strip, output_lines[start:end])) self.assertEqual(candidates, sorted(SQLITE_KEYWORDS)) From 4f1221e1d93749eda88e57264242c434de27d53f Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 16 May 2025 18:47:10 +0800 Subject: [PATCH 36/36] Rename 'test_completion_order' to 'test_completion_for_nothing' --- 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 613d165e596081..e90867d568e8f0 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -251,7 +251,7 @@ def test_nothing_to_complete(self): # to complete self.assertEqual(line_num, len(output_lines)) - def test_completion_order(self): + def test_completion_for_nothing(self): script = textwrap.dedent(""" import readline readline.parse_and_bind("set colored-completion-prefix off")