From 1b96be3eb95509c43bcb583ef0cb891cfef46a19 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 5 May 2025 02:21:04 +0800 Subject: [PATCH 01/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] =?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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] =?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/48] 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/48] 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/48] 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/48] 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/48] 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/48] =?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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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") From 3865131c6c6d91b01df2ac2f3bdcede302c7a30e Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 25 May 2025 12:57:43 +0800 Subject: [PATCH 37/48] Don't decrease reference for `PyModule_Add()` and `PyTuple_SetItem()` --- Modules/_sqlite/module.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index e8c3bdfd69c18a..1b73360fa69570 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -427,14 +427,10 @@ add_keyword_tuple(PyObject *module) goto error; } if (PyTuple_SetItem(keywords, i, kwd) < 0) { - Py_DECREF(kwd); goto error; } } - if (PyModule_Add(module, "SQLITE_KEYWORDS", keywords) < 0) { - goto error; - } - return 0; + return PyModule_Add(module, "SQLITE_KEYWORDS", keywords); error: Py_XDECREF(keywords); From ccd98a5e0caf95c77f77f6ec753a6e7b92ee2a00 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sat, 31 May 2025 13:49:52 +0800 Subject: [PATCH 38/48] Add @force_not_colorized_test_class Disable coloring in main process to see if it resolves buildbots failures --- Lib/test/test_sqlite3/test_cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index e90867d568e8f0..0ee3f5029ad546 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -207,6 +207,7 @@ def test_color(self): @requires_subprocess() +@force_not_colorized_test_class class CompletionTest(unittest.TestCase): PS1 = "sqlite> " From ffd0f028609939d4e67c0156f64f2e29254d196b Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 5 Jun 2025 20:57:15 +0800 Subject: [PATCH 39/48] Add two '\b\b'; Skip tests on FreeBSD --- Lib/test/test_sqlite3/test_cli.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 0ee3f5029ad546..16ec9e019d87bc 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -1,5 +1,6 @@ """sqlite3 CLI tests.""" import sqlite3 +import sys import textwrap import unittest @@ -240,8 +241,11 @@ def test_keyword_completion(self): self.assertIn(b"SELECT", output) self.assertIn(b"(1,)", output) + @unittest.skipIf(sys.platform.startswith("freebsd"), + "Two actual tabs are inserted when there are no matching" + " completions on FreeBSD") def test_nothing_to_complete(self): - input = b"xyzzy\t\t\b\b\b\b\b.quit\n" + input = b"xyzzy\t\t\b\b\b\b\b\b\b.quit\n" # set NO_COLOR to disable coloring for self.PS1 output = self.write_input(input, env={"NO_COLOR": "1"}) output_lines = output.decode().splitlines() From 6188a6d9141074e15dbfbb5ec0a84d830b3e84c7 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 5 Jun 2025 20:59:02 +0800 Subject: [PATCH 40/48] Amend skipping reason --- Lib/test/test_sqlite3/test_cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 16ec9e019d87bc..97818ea0f75468 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -243,7 +243,8 @@ def test_keyword_completion(self): @unittest.skipIf(sys.platform.startswith("freebsd"), "Two actual tabs are inserted when there are no matching" - " completions on FreeBSD") + " completions in the pseudo-terminal opened by run_pty()" + " on FreeBSD") def test_nothing_to_complete(self): input = b"xyzzy\t\t\b\b\b\b\b\b\b.quit\n" # set NO_COLOR to disable coloring for self.PS1 From 370dd8b1dba22f499ae8bc4f0bc5f881c948cfd4 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 5 Jun 2025 22:36:01 +0800 Subject: [PATCH 41/48] Remove comment 'set the keyword tuple' --- Modules/_sqlite/module.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 1b73360fa69570..0583d6499f4f08 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -735,7 +735,6 @@ module_exec(PyObject *module) goto error; } - /* Set the keyword tuple */ if (add_keyword_tuple(module) < 0) { goto error; } From 16b1674207788e0bb25d06656d890c8c0493f78f Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 5 Jun 2025 22:54:34 +0800 Subject: [PATCH 42/48] Disable keyword completion for SQLite<3.24.0 --- Lib/sqlite3/_completer.py | 3 ++- Modules/_sqlite/module.c | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py index fd18473fbcb815..9506d52f597993 100644 --- a/Lib/sqlite3/_completer.py +++ b/Lib/sqlite3/_completer.py @@ -1,10 +1,10 @@ -from _sqlite3 import SQLITE_KEYWORDS from contextlib import contextmanager _completion_matches = [] def _complete(text, state): + from _sqlite3 import SQLITE_KEYWORDS global _completion_matches if state == 0: text_upper = text.upper() @@ -19,6 +19,7 @@ def _complete(text, state): def enable_completer(): try: import readline + from _sqlite3 import SQLITE_KEYWORDS except ImportError: yield return diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 0583d6499f4f08..119b0cf9df684c 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -408,6 +408,9 @@ pysqlite_error_name(int rc) static int add_keyword_tuple(PyObject *module) { + if (sqlite3_libversion_number() < 3024000) { + return 0; + } int count = sqlite3_keyword_count(); PyObject *keywords = PyTuple_New(count); if (keywords == NULL) { From ea108baa43286aeba742823b4b67440fc6d5ec0d Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 5 Jun 2025 23:01:12 +0800 Subject: [PATCH 43/48] Don't disable the whole completion in case there will be more completion sources added which might be available for even old SQLite. --- Lib/sqlite3/_completer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py index 9506d52f597993..c859a8488fdc3a 100644 --- a/Lib/sqlite3/_completer.py +++ b/Lib/sqlite3/_completer.py @@ -4,7 +4,10 @@ def _complete(text, state): - from _sqlite3 import SQLITE_KEYWORDS + try: + from _sqlite3 import SQLITE_KEYWORDS + except ImportError: + SQLITE_KEYWORDS = () global _completion_matches if state == 0: text_upper = text.upper() @@ -19,7 +22,6 @@ def _complete(text, state): def enable_completer(): try: import readline - from _sqlite3 import SQLITE_KEYWORDS except ImportError: yield return From 13b527e62aef340e05e4d8a8f9379164fce9db92 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 5 Jun 2025 23:24:40 +0800 Subject: [PATCH 44/48] Use compile-time check --- Modules/_sqlite/module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 119b0cf9df684c..92515cddd459f3 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -408,7 +408,7 @@ pysqlite_error_name(int rc) static int add_keyword_tuple(PyObject *module) { - if (sqlite3_libversion_number() < 3024000) { + #if SQLITE_VERSION_NUMBER < 3024000 { return 0; } int count = sqlite3_keyword_count(); From fafd1bb2937e53a1a66218c5c480bcd85aef8079 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 5 Jun 2025 23:31:11 +0800 Subject: [PATCH 45/48] Correct #if usage --- Modules/_sqlite/module.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 92515cddd459f3..017f3443a1a438 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -408,9 +408,9 @@ pysqlite_error_name(int rc) static int add_keyword_tuple(PyObject *module) { - #if SQLITE_VERSION_NUMBER < 3024000 { + #if SQLITE_VERSION_NUMBER < 3024000 return 0; - } + #endif int count = sqlite3_keyword_count(); PyObject *keywords = PyTuple_New(count); if (keywords == NULL) { From 140818c5fd6f2d61faef85349556168816cf006b Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 6 Jun 2025 00:01:11 +0800 Subject: [PATCH 46/48] Wrap add_keyword_tuple() definition and its call in #if/#endif --- Modules/_sqlite/module.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 017f3443a1a438..fd73586a4171d9 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -405,12 +405,10 @@ pysqlite_error_name(int rc) return NULL; } +#if SQLITE_VERSION_NUMBER < 3024000 static int add_keyword_tuple(PyObject *module) { - #if SQLITE_VERSION_NUMBER < 3024000 - return 0; - #endif int count = sqlite3_keyword_count(); PyObject *keywords = PyTuple_New(count); if (keywords == NULL) { @@ -439,6 +437,7 @@ add_keyword_tuple(PyObject *module) Py_XDECREF(keywords); return -1; } +#endif static int add_integer_constants(PyObject *module) { @@ -738,9 +737,11 @@ module_exec(PyObject *module) goto error; } +#if SQLITE_VERSION_NUMBER < 3024000 if (add_keyword_tuple(module) < 0) { goto error; } +#endif if (PyModule_AddStringConstant(module, "sqlite_version", sqlite3_libversion())) { goto error; From fd6c89ef68f4384d720abe33c256fcc51ed49e1f Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 6 Jun 2025 13:00:24 +0200 Subject: [PATCH 47/48] Suggestions to python/cpython#133393 --- Lib/sqlite3/__main__.py | 4 +- Lib/sqlite3/_completer.py | 12 +++-- Lib/test/test_sqlite3/test_cli.py | 84 ++++++++++++++++++------------- Modules/_sqlite/module.c | 16 +++--- 4 files changed, 66 insertions(+), 50 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index ad4d33843acaaf..9e74b49ee828bc 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -12,7 +12,7 @@ from textwrap import dedent from _colorize import get_theme, theme_no_color -from ._completer import enable_completer +from ._completer import completer def execute(c, sql, suppress_errors=True, theme=theme_no_color): @@ -138,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 completer(): console = SqliteInteractiveConsole(con, use_color=True) console.interact(banner, exitmsg="") finally: diff --git a/Lib/sqlite3/_completer.py b/Lib/sqlite3/_completer.py index c859a8488fdc3a..f21ef69cad6439 100644 --- a/Lib/sqlite3/_completer.py +++ b/Lib/sqlite3/_completer.py @@ -1,14 +1,16 @@ from contextlib import contextmanager +try: + from _sqlite3 import SQLITE_KEYWORDS +except ImportError: + SQLITE_KEYWORDS = () + _completion_matches = [] def _complete(text, state): - try: - from _sqlite3 import SQLITE_KEYWORDS - except ImportError: - SQLITE_KEYWORDS = () global _completion_matches + if state == 0: text_upper = text.upper() _completion_matches = [c for c in SQLITE_KEYWORDS if c.startswith(text_upper)] @@ -19,7 +21,7 @@ def _complete(text, state): @contextmanager -def enable_completer(): +def completer(): try: import readline except ImportError: diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 97818ea0f75468..7f0b0f3650535a 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -4,7 +4,6 @@ import textwrap import unittest -from _sqlite3 import SQLITE_KEYWORDS from sqlite3.__main__ import main as cli from test.support.import_helper import import_module from test.support.os_helper import TESTFN, unlink @@ -209,35 +208,41 @@ def test_color(self): @requires_subprocess() @force_not_colorized_test_class -class CompletionTest(unittest.TestCase): +class Completion(unittest.TestCase): PS1 = "sqlite> " @classmethod def setUpClass(cls): + _sqlite3 = import_module("_sqlite3") + if not hasattr(_sqlite3, "SQLITE_KEYWORDS"): + raise unittest.SkipTest("unable to determine SQLite keywords") + readline = import_module("readline") if readline.backend == "editline": raise unittest.SkipTest("libedit readline is not supported") - def write_input(self, input, env=None): + def write_input(self, input_, env=None): script = textwrap.dedent(""" import readline + from sqlite3.__main__ import main + readline.parse_and_bind("set colored-completion-prefix off") - from sqlite3.__main__ import main; main() + main() """) - return run_pty(script, input, env) + return run_pty(script, input_, env) - def test_keyword_completion(self): + def test_complete_sql_keywords(self): # List candidates starting with 'S', there should be multiple matches. - input = b"S\t\tEL\t 1;\n.quit\n" - output = self.write_input(input) + input_ = b"S\t\tEL\t 1;\n.quit\n" + output = self.write_input(input_) 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 = self.write_input(input) + # Keywords are completed in upper case for even lower case user input. + input_ = b"sel\t\t 1;\n.quit\n" + output = self.write_input(input_) self.assertIn(b"SELECT", output) self.assertIn(b"(1,)", output) @@ -245,40 +250,51 @@ def test_keyword_completion(self): "Two actual tabs are inserted when there are no matching" " completions in the pseudo-terminal opened by run_pty()" " on FreeBSD") - def test_nothing_to_complete(self): - input = b"xyzzy\t\t\b\b\b\b\b\b\b.quit\n" - # 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")), -1) + def test_complete_no_match(self): + input_ = b"xyzzy\t\t\b\b\b\b\b\b\b.quit\n" + # Set NO_COLOR to disable coloring for self.PS1. + output = self.write_input(input_, env={"NO_COLOR": "1"}) + lines = output.decode().splitlines() + indices = ( + i for i, line in enumerate(lines, 1) + if line.startswith(f"{self.PS1}xyzzy") + ) + line_num = next(indices, -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)) + # Completions occupy lines, assert no extra lines when there is nothing + # to complete. + self.assertEqual(line_num, len(lines)) + + def test_complete_no_input(self): + from _sqlite3 import SQLITE_KEYWORDS - def test_completion_for_nothing(self): script = textwrap.dedent(""" import readline + from sqlite3.__main__ import main + + # Configure readline to ...: + # - hide control sequences surrounding each candidate + # - hide "Display all xxx possibilities? (y or n)" + # - hide "--More--" + # - show candidates one per line 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() + + main() """) - input = b"\t\t.quit\n" - output = run_pty(script, input, env={"NO_COLOR": "1"}) - output_lines = output.decode().splitlines() - indices = [i for i, line in enumerate(output_lines) - if line.startswith(self.PS1)] + input_ = b"\t\t.quit\n" + output = run_pty(script, input_, env={"NO_COLOR": "1"}) + lines = output.decode().splitlines() + indices = [ + i for i, line in enumerate(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])) + start, end = indices + candidates = [l.strip() for l in lines[start+1:end]] self.assertEqual(candidates, sorted(SQLITE_KEYWORDS)) diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index fd73586a4171d9..5464fd1227ad20 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -405,14 +405,14 @@ pysqlite_error_name(int rc) return NULL; } -#if SQLITE_VERSION_NUMBER < 3024000 static int add_keyword_tuple(PyObject *module) { +#if SQLITE_VERSION_NUMBER >= 3024000 int count = sqlite3_keyword_count(); PyObject *keywords = PyTuple_New(count); if (keywords == NULL) { - goto error; + return -1; } for (int i = 0; i < count; i++) { const char *keyword; @@ -427,17 +427,17 @@ add_keyword_tuple(PyObject *module) if (!kwd) { goto error; } - if (PyTuple_SetItem(keywords, i, kwd) < 0) { - goto error; - } + PyTuple_SET_ITEM(keywords, i, kwd); } return PyModule_Add(module, "SQLITE_KEYWORDS", keywords); error: - Py_XDECREF(keywords); + Py_DECREF(keywords); return -1; -} +#else + return 0; #endif +} static int add_integer_constants(PyObject *module) { @@ -737,11 +737,9 @@ module_exec(PyObject *module) goto error; } -#if SQLITE_VERSION_NUMBER < 3024000 if (add_keyword_tuple(module) < 0) { goto error; } -#endif if (PyModule_AddStringConstant(module, "sqlite_version", sqlite3_libversion())) { goto error; From 88c8d599e9f94cd3b3470e58908bbcc64367c51e Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 6 Jun 2025 14:23:35 +0200 Subject: [PATCH 48/48] Update Doc/whatsnew/3.15.rst --- 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 0407c87681eb76..c41620bb3a688e 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -137,7 +137,7 @@ shelve sqlite3 ------- -* Support keyword completion in the :mod:`sqlite3` command-line interface. +* Support SQL keyword completion in the :mod:`sqlite3` command-line interface. (Contributed by Long Tan in :gh:`133393`.) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy