From def2e81abe0a5df99eaf0d4f168b9eb144874ede Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Thu, 28 May 2020 10:36:33 +0200 Subject: [PATCH 01/33] Add proof-of-concept REPL --- Lib/sqlite3/__main__.py | 79 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 Lib/sqlite3/__main__.py diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py new file mode 100644 index 00000000000000..e5d9acc1c49c93 --- /dev/null +++ b/Lib/sqlite3/__main__.py @@ -0,0 +1,79 @@ +import sqlite3 + +from code import InteractiveConsole + + +class SqliteInteractiveConsole(InteractiveConsole): + + def __init__(self, database): + super().__init__() + self._con = sqlite3.connect(database, isolation_level=None) + self._cur = self._con.cursor() + + def runsql(self, sql): + if sqlite3.complete_statement(sql): + try: + self._cur.execute(sql) + rows = self._cur.fetchall() + for row in rows: + print(row) + except sqlite3.Error as e: + print(f"{e.sqlite_errorname}: {e}") + return False + return True + + def runpy(self, source, filename="", symbol="single"): + code = self.compile(source, filename, symbol) + self.runcode(code) + return False + + def printhelp(self, ignored): + print("Enter SQL code and press enter.") + + def runsource(self, source, filename="", symbol="single"): + keywords = { + "version": lambda x: print(f"{sqlite3.sqlite_version}"), + "help": self.printhelp, + "copyright": self.runpy, + "credits": self.runpy, + "license": self.runpy, + "license()": self.runpy, + "quit()": self.runpy, + "quit": self.runpy, + } + return keywords.get(source, self.runsql)(source) + + +if __name__ == "__main__": + import sys + from argparse import ArgumentParser + from textwrap import dedent + + parser = ArgumentParser( + description="Python sqlite3 REPL", + prog="python -m sqlite3", + ) + parser.add_argument( + "-f", "--filename", + type=str, dest="database", action="store", default=":memory:", + help="Database to open (default in-memory database)", + ) + args = parser.parse_args() + + if args.database == ":memory:": + db_name = "a transient in-memory database" + else: + db_name = f"'{args.database}'" + + banner = dedent(f""" + sqlite3 shell, running on SQLite version {sqlite3.sqlite_version} + Connected to {db_name} + + Each command will be run using execute() on the cursor. + Type "help" for more information; type "quit" or CTRL-D to quit. + """).strip() + sys.ps1 = "sqlite> " + sys.ps2 = " ... " + + console = SqliteInteractiveConsole(args.database) + console.interact(banner, exitmsg="") From 1f01905e133c92ffa0354b548321193ba70a598d Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 20 Jul 2022 00:24:09 +0200 Subject: [PATCH 02/33] Add NEWS --- .../next/Library/2022-07-20-00-23-58.gh-issue-77617.XGaqSQ.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2022-07-20-00-23-58.gh-issue-77617.XGaqSQ.rst diff --git a/Misc/NEWS.d/next/Library/2022-07-20-00-23-58.gh-issue-77617.XGaqSQ.rst b/Misc/NEWS.d/next/Library/2022-07-20-00-23-58.gh-issue-77617.XGaqSQ.rst new file mode 100644 index 00000000000000..b685a737fcdf70 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-07-20-00-23-58.gh-issue-77617.XGaqSQ.rst @@ -0,0 +1 @@ +Add interactive shell for :mod:`sqlite3`. Patch by Erlend Aasland. From 86a456691574400d89a1b3bb57fb9f412a214a25 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 20 Jul 2022 17:25:33 +0200 Subject: [PATCH 03/33] Remove redundant __name__ condition --- Lib/sqlite3/__main__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index e5d9acc1c49c93..3a9289d3d382ff 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -1,6 +1,9 @@ import sqlite3 +import sys +from argparse import ArgumentParser from code import InteractiveConsole +from textwrap import dedent class SqliteInteractiveConsole(InteractiveConsole): @@ -44,11 +47,7 @@ def runsource(self, source, filename="", symbol="single"): return keywords.get(source, self.runsql)(source) -if __name__ == "__main__": - import sys - from argparse import ArgumentParser - from textwrap import dedent - +def main(): parser = ArgumentParser( description="Python sqlite3 REPL", prog="python -m sqlite3", @@ -77,3 +76,6 @@ def runsource(self, source, filename="", symbol="single"): console = SqliteInteractiveConsole(args.database) console.interact(banner, exitmsg="") + + +main() From 896834c6dd2bb849d178eb41fc5188fe06c8f8b9 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 20 Jul 2022 17:35:42 +0200 Subject: [PATCH 04/33] Add -v argument for dumping SQLite version --- Lib/sqlite3/__main__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 3a9289d3d382ff..ee000b8b4f076a 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -47,6 +47,10 @@ def runsource(self, source, filename="", symbol="single"): return keywords.get(source, self.runsql)(source) +def dump_version(): + print(f"SQLite version {sqlite3.sqlite_version}") + + def main(): parser = ArgumentParser( description="Python sqlite3 REPL", @@ -57,7 +61,14 @@ def main(): type=str, dest="database", action="store", default=":memory:", help="Database to open (default in-memory database)", ) + parser.add_argument( + "-v", "--version", + dest="show_version", action="store_true", default=False, + help="Print SQLite version", + ) args = parser.parse_args() + if args.show_version: + return dump_version() if args.database == ":memory:": db_name = "a transient in-memory database" From 4ba731ac1669389be788d49304f4b52158bbe80b Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 20 Jul 2022 20:51:32 +0200 Subject: [PATCH 05/33] Address review: close connection explicitly --- Lib/sqlite3/__main__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index ee000b8b4f076a..18fcaa895c526d 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -13,6 +13,9 @@ def __init__(self, database): self._con = sqlite3.connect(database, isolation_level=None) self._cur = self._con.cursor() + def __del__(self): + self._con.close() + def runsql(self, sql): if sqlite3.complete_statement(sql): try: From 8179a68e8e031f65e9db4f28575a05541a468ad1 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 29 Jul 2022 12:09:57 +0200 Subject: [PATCH 06/33] Add docs --- Doc/library/sqlite3.rst | 17 +++++++++++++++++ Lib/sqlite3/__main__.py | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 6a430f00aea4d1..1bc7ef5825cf0c 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -1718,6 +1718,23 @@ the context manager is a no-op. .. literalinclude:: ../includes/sqlite3/ctx_manager.py +Command-line interface +---------------------- + +The ``sqlite3`` module can be invoked as a script +in order to provide a simple SQLite shell. + +.. program:: python -m sqlite3 + +.. cmdoption:: -f, --filename + Database to open (defaults to ``':memory:'``). + +.. cmdoption:: -v, --version + Print underlying SQLite library version. + +.. versionadded:: 3.12 + + .. rubric:: Footnotes .. [#f1] The sqlite3 module is not built with loadable extension support by diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 18fcaa895c526d..3b9498ffe3c516 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -62,12 +62,12 @@ def main(): parser.add_argument( "-f", "--filename", type=str, dest="database", action="store", default=":memory:", - help="Database to open (default in-memory database)", + help="Database to open (defaults to ':memory:')", ) parser.add_argument( "-v", "--version", dest="show_version", action="store_true", default=False, - help="Print SQLite version", + help="Print underlying SQLite library version", ) args = parser.parse_args() if args.show_version: From 91a77b7c906361d71ea53e7e869aff3ea1661174 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 29 Jul 2022 12:11:17 +0200 Subject: [PATCH 07/33] Remove copyright, credits, and license cli commands --- Lib/sqlite3/__main__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 3b9498ffe3c516..9343ea28ba6bcf 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -40,10 +40,6 @@ def runsource(self, source, filename="", symbol="single"): keywords = { "version": lambda x: print(f"{sqlite3.sqlite_version}"), "help": self.printhelp, - "copyright": self.runpy, - "credits": self.runpy, - "license": self.runpy, - "license()": self.runpy, "quit()": self.runpy, "quit": self.runpy, } From 2f27a049e82394e83e8dbafd807826e2131f4f5d Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 29 Jul 2022 12:12:40 +0200 Subject: [PATCH 08/33] Document how to quit --- Doc/library/sqlite3.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 1bc7ef5825cf0c..20735f1c0f27e7 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -1723,6 +1723,7 @@ Command-line interface The ``sqlite3`` module can be invoked as a script in order to provide a simple SQLite shell. +Type ``quit`` or CTRL-D to exit the shell. .. program:: python -m sqlite3 From ae55e8e86532b62b0f58f772e34575a665055227 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 29 Jul 2022 12:44:50 +0200 Subject: [PATCH 09/33] Fix sphinx option refs by moving the footnote up to where it belongs, and making it a note --- Doc/library/sqlite3.rst | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 20735f1c0f27e7..0f99d711254732 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -753,7 +753,14 @@ Connection Objects aggregates or whole new virtual table implementations. One well-known extension is the fulltext-search extension distributed with SQLite. - Loadable extensions are disabled by default. See [#f1]_. + .. note:: + + The ``sqlite3`` module is not built with loadable extension support by + default, because some platforms (notably macOS) have SQLite + libraries which are compiled without this feature. + To get loadable extension support, + you must pass the :option:`--enable-loadable-sqlite-extensions` option + to :program:`configure`. .. audit-event:: sqlite3.enable_load_extension connection,enabled sqlite3.Connection.enable_load_extension @@ -1727,19 +1734,10 @@ Type ``quit`` or CTRL-D to exit the shell. .. program:: python -m sqlite3 -.. cmdoption:: -f, --filename +.. option:: -f, --filename Database to open (defaults to ``':memory:'``). -.. cmdoption:: -v, --version +.. option:: -v, --version Print underlying SQLite library version. .. versionadded:: 3.12 - - -.. rubric:: Footnotes - -.. [#f1] The sqlite3 module is not built with loadable extension support by - default, because some platforms (notably macOS) have SQLite - libraries which are compiled without this feature. To get loadable - extension support, you must pass the - :option:`--enable-loadable-sqlite-extensions` option to configure. From 024cd90e94a3ea750f566b21ef80afe7e6efc3e7 Mon Sep 17 00:00:00 2001 From: Erlend Egeberg Aasland Date: Fri, 29 Jul 2022 23:22:26 +0200 Subject: [PATCH 10/33] Address review: iterate over cursor Co-authored-by: Serhiy Storchaka --- Lib/sqlite3/__main__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 9343ea28ba6bcf..b1022a92dea092 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -19,9 +19,7 @@ def __del__(self): def runsql(self, sql): if sqlite3.complete_statement(sql): try: - self._cur.execute(sql) - rows = self._cur.fetchall() - for row in rows: + for row in self._cur.execute(sql): print(row) except sqlite3.Error as e: print(f"{e.sqlite_errorname}: {e}") From dee441a7b16e01eee1f5227fcd8b6c35cad92a45 Mon Sep 17 00:00:00 2001 From: Erlend Egeberg Aasland Date: Fri, 29 Jul 2022 23:23:01 +0200 Subject: [PATCH 11/33] Address review: repr iso. f-string Co-authored-by: Serhiy Storchaka --- Lib/sqlite3/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index b1022a92dea092..43a1fe082efeaf 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -70,7 +70,7 @@ def main(): if args.database == ":memory:": db_name = "a transient in-memory database" else: - db_name = f"'{args.database}'" + db_name = repr(args.database) banner = dedent(f""" sqlite3 shell, running on SQLite version {sqlite3.sqlite_version} From ec04ea3fa33107be88e61548755172c9ac6905da Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 29 Jul 2022 23:26:07 +0200 Subject: [PATCH 12/33] Address review: pass connection iso. path --- Lib/sqlite3/__main__.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 43a1fe082efeaf..1d847a3916ec34 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -8,13 +8,10 @@ class SqliteInteractiveConsole(InteractiveConsole): - def __init__(self, database): + def __init__(self, connection): super().__init__() - self._con = sqlite3.connect(database, isolation_level=None) - self._cur = self._con.cursor() - - def __del__(self): - self._con.close() + self._con = connection + self._cur = connection.cursor() def runsql(self, sql): if sqlite3.complete_statement(sql): @@ -82,8 +79,10 @@ def main(): sys.ps1 = "sqlite> " sys.ps2 = " ... " - console = SqliteInteractiveConsole(args.database) + con = sqlite3.connect(args.database, isolation_level=None) + console = SqliteInteractiveConsole(con) console.interact(banner, exitmsg="") + con.close() main() From 9b792309bdee9178ad7aa6aee489e0025d1829a6 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 29 Jul 2022 23:28:02 +0200 Subject: [PATCH 13/33] Address review: use argparse version trick --- Lib/sqlite3/__main__.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 1d847a3916ec34..bf7f65f9a00028 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -41,10 +41,6 @@ def runsource(self, source, filename="", symbol="single"): return keywords.get(source, self.runsql)(source) -def dump_version(): - print(f"SQLite version {sqlite3.sqlite_version}") - - def main(): parser = ArgumentParser( description="Python sqlite3 REPL", @@ -56,13 +52,11 @@ def main(): help="Database to open (defaults to ':memory:')", ) parser.add_argument( - "-v", "--version", - dest="show_version", action="store_true", default=False, + "-v", "--version", action="version", + version=f"SQLite version {sqlite3.sqlite_version}", help="Print underlying SQLite library version", ) args = parser.parse_args() - if args.show_version: - return dump_version() if args.database == ":memory:": db_name = "a transient in-memory database" From 74fb53d30542fb32732bc64ef4fddae65966db5d Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 29 Jul 2022 23:37:49 +0200 Subject: [PATCH 14/33] Address review: use . prefix for commands, and sys.exit for quitting --- Doc/library/sqlite3.rst | 2 +- Lib/sqlite3/__main__.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 454c034fa68074..8f0531fd6252d1 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -1728,7 +1728,7 @@ Command-line interface The ``sqlite3`` module can be invoked as a script in order to provide a simple SQLite shell. -Type ``quit`` or CTRL-D to exit the shell. +Type ``.quit`` or CTRL-D to exit the shell. .. program:: python -m sqlite3 diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index bf7f65f9a00028..3964ba98ebfad1 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -33,10 +33,9 @@ def printhelp(self, ignored): def runsource(self, source, filename="", symbol="single"): keywords = { - "version": lambda x: print(f"{sqlite3.sqlite_version}"), - "help": self.printhelp, - "quit()": self.runpy, - "quit": self.runpy, + ".version": lambda x: print(f"{sqlite3.sqlite_version}"), + ".help": self.printhelp, + ".quit": lambda x: sys.exit(0), } return keywords.get(source, self.runsql)(source) @@ -68,7 +67,7 @@ def main(): Connected to {db_name} Each command will be run using execute() on the cursor. - Type "help" for more information; type "quit" or CTRL-D to quit. + Type ".help" for more information; type ".quit" or CTRL-D to quit. """).strip() sys.ps1 = "sqlite> " sys.ps2 = " ... " From 1d62ccbd65c10363b7c192b8fcf3bf8b2fba7f5e Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 29 Jul 2022 23:38:58 +0200 Subject: [PATCH 15/33] Address review: reduce indent level --- Lib/sqlite3/__main__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 3964ba98ebfad1..d7455b51384425 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -14,14 +14,14 @@ def __init__(self, connection): self._cur = connection.cursor() def runsql(self, sql): - if sqlite3.complete_statement(sql): - try: - for row in self._cur.execute(sql): - print(row) - except sqlite3.Error as e: - print(f"{e.sqlite_errorname}: {e}") - return False - return True + if not sqlite3.complete_statement(sql): + return True + try: + for row in self._cur.execute(sql): + print(row) + except sqlite3.Error as e: + print(f"{e.sqlite_errorname}: {e}") + return False def runpy(self, source, filename="", symbol="single"): code = self.compile(source, filename, symbol) From 340b896ce440c1970eec9fbdbbb6eaa2ea2465b2 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sat, 30 Jul 2022 21:56:43 +0200 Subject: [PATCH 16/33] Add filename and sql args a la sqlite3 cli --- Lib/sqlite3/__main__.py | 50 +++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index d7455b51384425..8e23c6667c4b65 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -6,9 +6,22 @@ from textwrap import dedent +def execute(c, sql): + try: + for row in c.execute(sql): + print(row) + except sqlite3.Error as e: + tp = type(e).__name__ + try: + print(f"{tp} ({e.sqlite_errorname}): {e}") + except: + print(f"{tp}: {e}") + return None + + class SqliteInteractiveConsole(InteractiveConsole): - def __init__(self, connection): + def __init__(self, connection, sql): super().__init__() self._con = connection self._cur = connection.cursor() @@ -16,11 +29,7 @@ def __init__(self, connection): def runsql(self, sql): if not sqlite3.complete_statement(sql): return True - try: - for row in self._cur.execute(sql): - print(row) - except sqlite3.Error as e: - print(f"{e.sqlite_errorname}: {e}") + execute(self._cur, sql) return False def runpy(self, source, filename="", symbol="single"): @@ -30,6 +39,7 @@ def runpy(self, source, filename="", symbol="single"): def printhelp(self, ignored): print("Enter SQL code and press enter.") + return None def runsource(self, source, filename="", symbol="single"): keywords = { @@ -46,9 +56,18 @@ def main(): prog="python -m sqlite3", ) parser.add_argument( - "-f", "--filename", - type=str, dest="database", action="store", default=":memory:", - help="Database to open (defaults to ':memory:')", + "filename", type=str, default=":memory:", nargs="?", + help=( + "SQLite database to open (defaults to ':memory:'). " + "A new database is created if the file does not previously exist." + ), + ) + parser.add_argument( + "sql", type=str, nargs="?", + help=( + "An SQL query to execute. " + "Any returned rows are printed to stdout." + ), ) parser.add_argument( "-v", "--version", action="version", @@ -57,10 +76,10 @@ def main(): ) args = parser.parse_args() - if args.database == ":memory:": + if args.filename == ":memory:": db_name = "a transient in-memory database" else: - db_name = repr(args.database) + db_name = repr(args.filename) banner = dedent(f""" sqlite3 shell, running on SQLite version {sqlite3.sqlite_version} @@ -72,9 +91,12 @@ def main(): sys.ps1 = "sqlite> " sys.ps2 = " ... " - con = sqlite3.connect(args.database, isolation_level=None) - console = SqliteInteractiveConsole(con) - console.interact(banner, exitmsg="") + con = sqlite3.connect(args.filename, isolation_level=None) + if args.sql: + execute(con, args.sql) + else: + console = SqliteInteractiveConsole(con) + console.interact(banner, exitmsg="") con.close() From d906361d6309886934cf234f8cbed8041293237f Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 31 Jul 2022 00:52:08 +0200 Subject: [PATCH 17/33] Fix constructor --- Lib/sqlite3/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 8e23c6667c4b65..9d73d1c37db09b 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -21,7 +21,7 @@ def execute(c, sql): class SqliteInteractiveConsole(InteractiveConsole): - def __init__(self, connection, sql): + def __init__(self, connection): super().__init__() self._con = connection self._cur = connection.cursor() From e728e179de26d4e0b88f1c322b1624f7323e30db Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 31 Jul 2022 08:56:25 +0200 Subject: [PATCH 18/33] Address Serhiy's second round of review --- Doc/library/sqlite3.rst | 6 +++--- Lib/sqlite3/__main__.py | 21 +++++++++------------ 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index bc145710225bf3..6367b347ed7c57 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -1752,10 +1752,10 @@ The ``sqlite3`` module can be invoked as a script in order to provide a simple SQLite shell. Type ``.quit`` or CTRL-D to exit the shell. -.. program:: python -m sqlite3 +.. program:: python -m sqlite3 [-h] [-v] [filename] [sql] -.. option:: -f, --filename - Database to open (defaults to ``':memory:'``). +.. option:: -h, --help + Print CLI help. .. option:: -v, --version Print underlying SQLite library version. diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 9d73d1c37db09b..36d61f2556b2e0 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -16,7 +16,6 @@ def execute(c, sql): print(f"{tp} ({e.sqlite_errorname}): {e}") except: print(f"{tp}: {e}") - return None class SqliteInteractiveConsole(InteractiveConsole): @@ -37,14 +36,10 @@ def runpy(self, source, filename="", symbol="single"): self.runcode(code) return False - def printhelp(self, ignored): - print("Enter SQL code and press enter.") - return None - def runsource(self, source, filename="", symbol="single"): keywords = { ".version": lambda x: print(f"{sqlite3.sqlite_version}"), - ".help": self.printhelp, + ".help": lambda x: print("Enter SQL code and press enter."), ".quit": lambda x: sys.exit(0), } return keywords.get(source, self.runsql)(source) @@ -92,12 +87,14 @@ def main(): sys.ps2 = " ... " con = sqlite3.connect(args.filename, isolation_level=None) - if args.sql: - execute(con, args.sql) - else: - console = SqliteInteractiveConsole(con) - console.interact(banner, exitmsg="") - con.close() + try: + if args.sql: + execute(con, args.sql) + else: + console = SqliteInteractiveConsole(con) + console.interact(banner, exitmsg="") + finally: + con.close() main() From e2e24b8550ecb4ec8bcd15dd1e46726f2ecf87a8 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 31 Jul 2022 09:01:01 +0200 Subject: [PATCH 19/33] Remove useless function and lambda params --- Lib/sqlite3/__main__.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 36d61f2556b2e0..6f2a3fa9cc8bf1 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -31,18 +31,16 @@ def runsql(self, sql): execute(self._cur, sql) return False - def runpy(self, source, filename="", symbol="single"): - code = self.compile(source, filename, symbol) - self.runcode(code) - return False - def runsource(self, source, filename="", symbol="single"): keywords = { - ".version": lambda x: print(f"{sqlite3.sqlite_version}"), - ".help": lambda x: print("Enter SQL code and press enter."), - ".quit": lambda x: sys.exit(0), + ".version": lambda: print(f"{sqlite3.sqlite_version}"), + ".help": lambda: print("Enter SQL code and press enter."), + ".quit": lambda: sys.exit(0), } - return keywords.get(source, self.runsql)(source) + if source in keywords: + keywords[source]() + else: + self.runsql(source) def main(): From 294acb9a69be69686a11fa2060a5c6ad64545a18 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 31 Jul 2022 10:30:54 +0200 Subject: [PATCH 20/33] Move CLI docs to Reference --- Doc/library/sqlite3.rst | 36 ++++++++++++++++++------------------ Lib/sqlite3/__main__.py | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 6367b347ed7c57..01686da04e43dd 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -1439,6 +1439,24 @@ and you can let the ``sqlite3`` module convert SQLite types to Python types via :ref:`converters `. +Command-line interface +^^^^^^^^^^^^^^^^^^^^^^ + +The ``sqlite3`` module can be invoked as a script +in order to provide a simple SQLite shell. +Type ``.quit`` or CTRL-D to exit the shell. + +.. program:: python -m sqlite3 [-h] [-v] [filename] [sql] + +.. option:: -h, --help + Print CLI help. + +.. option:: -v, --version + Print underlying SQLite library version. + +.. versionadded:: 3.12 + + .. _sqlite3-howtos: How-to guides @@ -1743,21 +1761,3 @@ regardless of the value of :attr:`~Connection.isolation_level`. .. _SQLite transaction behaviour: https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions - - -Command-line interface ----------------------- - -The ``sqlite3`` module can be invoked as a script -in order to provide a simple SQLite shell. -Type ``.quit`` or CTRL-D to exit the shell. - -.. program:: python -m sqlite3 [-h] [-v] [filename] [sql] - -.. option:: -h, --help - Print CLI help. - -.. option:: -v, --version - Print underlying SQLite library version. - -.. versionadded:: 3.12 diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 6f2a3fa9cc8bf1..096c634f01d6d5 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -45,7 +45,7 @@ def runsource(self, source, filename="", symbol="single"): def main(): parser = ArgumentParser( - description="Python sqlite3 REPL", + description="Python sqlite3 CLI", prog="python -m sqlite3", ) parser.add_argument( From 3cfff7fc3442b02eb18024b74b7801c078907263 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 31 Jul 2022 10:48:04 +0200 Subject: [PATCH 21/33] Improve NEWS and add What's New --- Doc/library/sqlite3.rst | 2 ++ Doc/whatsnew/3.12.rst | 7 +++++++ .../Library/2022-07-20-00-23-58.gh-issue-77617.XGaqSQ.rst | 3 ++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 01686da04e43dd..4ecb408bbe9ea2 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -1439,6 +1439,8 @@ and you can let the ``sqlite3`` module convert SQLite types to Python types via :ref:`converters `. +.. _sqlite3-cli: + Command-line interface ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 0c53bc0c1111d7..67396f8e02280b 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -112,6 +112,13 @@ os (Contributed by Kumar Aditya in :gh:`93312`.) +sqlite3 +------- + +* Add a :ref:`command-line interface `. + (Contributed by Erlend E. Aasland in :gh:`77617`.) + + Optimizations ============= diff --git a/Misc/NEWS.d/next/Library/2022-07-20-00-23-58.gh-issue-77617.XGaqSQ.rst b/Misc/NEWS.d/next/Library/2022-07-20-00-23-58.gh-issue-77617.XGaqSQ.rst index b685a737fcdf70..1cbaa7dfe15ec2 100644 --- a/Misc/NEWS.d/next/Library/2022-07-20-00-23-58.gh-issue-77617.XGaqSQ.rst +++ b/Misc/NEWS.d/next/Library/2022-07-20-00-23-58.gh-issue-77617.XGaqSQ.rst @@ -1 +1,2 @@ -Add interactive shell for :mod:`sqlite3`. Patch by Erlend Aasland. +Add :mod:`sqlite3` :ref:`command-line interface `. +Patch by Erlend Aasland. From 82c753cf77e6588cddb7404dbb5e9b47ca094df9 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 31 Jul 2022 12:05:34 +0200 Subject: [PATCH 22/33] Partially address Kumar's review: use narrow exception --- Lib/sqlite3/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 096c634f01d6d5..fa74b5fc11a2f4 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -14,7 +14,7 @@ def execute(c, sql): tp = type(e).__name__ try: print(f"{tp} ({e.sqlite_errorname}): {e}") - except: + except AttributeError: print(f"{tp}: {e}") From 36454ab619d7527bd92ba5480412a8c5258f474a Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 31 Jul 2022 12:08:08 +0200 Subject: [PATCH 23/33] Partially address more of Kumar's review: add guard --- Lib/sqlite3/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index fa74b5fc11a2f4..5c01cd542c3abb 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -95,4 +95,6 @@ def main(): con.close() -main() +# This file can be invoked when the module is imported, hence the guard: +if __name__ == "__main__": + main() From 625e2e351e8811e8061244049f85c4a4162ed441 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 31 Jul 2022 12:30:58 +0200 Subject: [PATCH 24/33] Use pattern matching --- Lib/sqlite3/__main__.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 5c01cd542c3abb..3f588774508aad 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -32,15 +32,16 @@ def runsql(self, sql): return False def runsource(self, source, filename="", symbol="single"): - keywords = { - ".version": lambda: print(f"{sqlite3.sqlite_version}"), - ".help": lambda: print("Enter SQL code and press enter."), - ".quit": lambda: sys.exit(0), - } - if source in keywords: - keywords[source]() - else: - self.runsql(source) + match source: + case ".version": + print(f"{sqlite3.sqlite_version}") + case ".help": + print("Enter SQL code and press enter.") + case ".quit": + sys.exit(0) + case _: + return self.runsql(source) + return False def main(): From f624873a3eaa0eeb8c2f70faf40b963de9c8656d Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 31 Jul 2022 22:08:09 +0200 Subject: [PATCH 25/33] Revert "Partially address more of Kumar's review: add guard" This reverts commit 36454ab619d7527bd92ba5480412a8c5258f474a. --- Lib/sqlite3/__main__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 3f588774508aad..84659be52f0b2d 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -96,6 +96,4 @@ def main(): con.close() -# This file can be invoked when the module is imported, hence the guard: -if __name__ == "__main__": - main() +main() From df75f5f6c994e05dab07f598684a91d39b746899 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 31 Jul 2022 23:43:25 +0200 Subject: [PATCH 26/33] Print errors to stderr iso. stdout --- Lib/sqlite3/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 84659be52f0b2d..18be20c22d40ea 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -13,9 +13,9 @@ def execute(c, sql): except sqlite3.Error as e: tp = type(e).__name__ try: - print(f"{tp} ({e.sqlite_errorname}): {e}") + print(f"{tp} ({e.sqlite_errorname}): {e}", file=sys.stderr) except AttributeError: - print(f"{tp}: {e}") + print(f"{tp}: {e}", file=sys.stderr) class SqliteInteractiveConsole(InteractiveConsole): From 44c2571132a5e04f0ef48e5444a57833e1965b5f Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 31 Jul 2022 23:48:13 +0200 Subject: [PATCH 27/33] Non-zero exit for SQL errors passed from command line --- Lib/sqlite3/__main__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 18be20c22d40ea..b416de209d7de7 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -6,7 +6,7 @@ from textwrap import dedent -def execute(c, sql): +def execute(c, sql, suppress_errors=True): try: for row in c.execute(sql): print(row) @@ -16,6 +16,8 @@ def execute(c, sql): print(f"{tp} ({e.sqlite_errorname}): {e}", file=sys.stderr) except AttributeError: print(f"{tp}: {e}", file=sys.stderr) + if not suppress_errors: + sys.exit(1) class SqliteInteractiveConsole(InteractiveConsole): @@ -88,7 +90,7 @@ def main(): con = sqlite3.connect(args.filename, isolation_level=None) try: if args.sql: - execute(con, args.sql) + execute(con, args.sql, suppress_errors=False) else: console = SqliteInteractiveConsole(con) console.interact(banner, exitmsg="") From d640458bf2b48069be51a82c62488b485183eefc Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 31 Jul 2022 23:49:44 +0200 Subject: [PATCH 28/33] Add tests --- Lib/test/test_sqlite3/test_cli.py | 155 ++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 Lib/test/test_sqlite3/test_cli.py diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py new file mode 100644 index 00000000000000..44d7c00377cdba --- /dev/null +++ b/Lib/test/test_sqlite3/test_cli.py @@ -0,0 +1,155 @@ +"""sqlite3 CLI tests.""" + +import sqlite3 as sqlite +import subprocess +import sys +import unittest + +from test.support import SHORT_TIMEOUT, requires_subprocess +from test.support.os_helper import TESTFN, unlink + + +@requires_subprocess() +class CommandLineInterface(unittest.TestCase): + + def _do_test(self, *args, expect_success=True): + with subprocess.Popen( + [sys.executable, "-m", "sqlite3", *args], + encoding="utf-8", + bufsize=0, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) as proc: + proc.wait() + if expect_success == bool(proc.returncode): + self.fail("".join(proc.stderr)) + stdout = proc.stdout.read() + stderr = proc.stderr.read() + if expect_success: + self.assertEqual(stderr, "") + else: + self.assertEqual(stdout, "") + return stdout, stderr + + def expect_success(self, *args): + out, _ = self._do_test(*args) + return out + + def expect_failure(self, *args): + _, err = self._do_test(*args, expect_success=False) + return err + + def test_cli_help(self): + out = self.expect_success("-h") + self.assertIn("usage: python -m sqlite3", out) + + def test_cli_version(self): + out = self.expect_success("-v") + self.assertIn(sqlite.sqlite_version, out) + + def test_cli_execute_sql(self): + out = self.expect_success(":memory:", "select 1") + self.assertIn("(1,)", out) + + def test_cli_execute_too_much_sql(self): + stderr = self.expect_failure(":memory:", "select 1; select 2") + err = "ProgrammingError: You can only execute one statement at a time" + self.assertIn(err, stderr) + + def test_cli_execute_incomplete_sql(self): + stderr = self.expect_failure(":memory:", "sel") + self.assertIn("OperationalError (SQLITE_ERROR)", stderr) + + def test_cli_on_disk_db(self): + out = self.expect_success(TESTFN, "create table t(t)") + self.addCleanup(unlink, TESTFN) + self.assertEqual(out, "") + out = self.expect_success(TESTFN, "select count(t) from t") + self.assertIn("(0,)", out) + + +@requires_subprocess() +class InteractiveSession(unittest.TestCase): + TIMEOUT = SHORT_TIMEOUT / 10. + MEMORY_DB_MSG = "Connected to a transient in-memory database" + PS1 = "sqlite> " + PS2 = "... " + + def start_cli(self, *args): + return subprocess.Popen( + [sys.executable, "-m", "sqlite3", *args], + encoding="utf-8", + bufsize=0, + stdin=subprocess.PIPE, + # Note: the banner is printed to stderr, the prompt to stdout. + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + def expect_success(self, proc, err): + proc.wait() + if proc.returncode: + self.fail("".join(err)) + + def test_interact(self): + with self.start_cli() as proc: + out, err = proc.communicate(timeout=self.TIMEOUT) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertIn(self.PS1, out) + self.expect_success(proc, err) + + def test_interact_quit(self): + with self.start_cli() as proc: + out, err = proc.communicate(input=".quit", timeout=self.TIMEOUT) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertIn(self.PS1, out) + self.expect_success(proc, err) + + def test_interact_version(self): + with self.start_cli() as proc: + out, err = proc.communicate(input=".version", timeout=self.TIMEOUT) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertIn(sqlite.sqlite_version, out) + self.expect_success(proc, err) + + def test_interact_valid_sql(self): + with self.start_cli() as proc: + out, err = proc.communicate(input="select 1;", + timeout=self.TIMEOUT) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertIn("(1,)", out) + self.expect_success(proc, err) + + def test_interact_valid_multiline_sql(self): + with self.start_cli() as proc: + out, err = proc.communicate(input="select 1\n;", + timeout=self.TIMEOUT) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertIn(self.PS2, out) + self.assertIn("(1,)", out) + self.expect_success(proc, err) + + def test_interact_invalid_sql(self): + with self.start_cli() as proc: + out, err = proc.communicate(input="sel;", timeout=self.TIMEOUT) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertIn("OperationalError (SQLITE_ERROR)", err) + self.expect_success(proc, err) + + def test_interact_on_disk_file(self): + with self.start_cli(TESTFN) as proc: + out, err = proc.communicate(input="create table t(t);", + timeout=self.TIMEOUT) + self.assertIn(TESTFN, err) + self.assertIn(self.PS1, out) + self.expect_success(proc, err) + self.addCleanup(unlink, TESTFN) + with self.start_cli(TESTFN, "select count(t) from t") as proc: + out = proc.stdout.read() + err = proc.stderr.read() + self.assertIn("(0,)", out) + self.expect_success(proc, err) + + +if __name__ == "__main__": + unittest.main() From 0997004261babc0a7bc0178d9a96446c3866c28d Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 31 Jul 2022 23:58:49 +0200 Subject: [PATCH 29/33] Simplify interactive tests --- Lib/test/test_sqlite3/test_cli.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py index 44d7c00377cdba..6cdade54b07d6b 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -86,31 +86,31 @@ def start_cli(self, *args): stderr=subprocess.PIPE, ) - def expect_success(self, proc, err): + def expect_success(self, proc): proc.wait() if proc.returncode: - self.fail("".join(err)) + self.fail("".join(proc.stderr)) def test_interact(self): with self.start_cli() as proc: out, err = proc.communicate(timeout=self.TIMEOUT) self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn(self.PS1, out) - self.expect_success(proc, err) + self.expect_success(proc) def test_interact_quit(self): with self.start_cli() as proc: out, err = proc.communicate(input=".quit", timeout=self.TIMEOUT) self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn(self.PS1, out) - self.expect_success(proc, err) + self.expect_success(proc) def test_interact_version(self): with self.start_cli() as proc: out, err = proc.communicate(input=".version", timeout=self.TIMEOUT) self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn(sqlite.sqlite_version, out) - self.expect_success(proc, err) + self.expect_success(proc) def test_interact_valid_sql(self): with self.start_cli() as proc: @@ -118,7 +118,7 @@ def test_interact_valid_sql(self): timeout=self.TIMEOUT) self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn("(1,)", out) - self.expect_success(proc, err) + self.expect_success(proc) def test_interact_valid_multiline_sql(self): with self.start_cli() as proc: @@ -127,14 +127,14 @@ def test_interact_valid_multiline_sql(self): self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn(self.PS2, out) self.assertIn("(1,)", out) - self.expect_success(proc, err) + self.expect_success(proc) def test_interact_invalid_sql(self): with self.start_cli() as proc: out, err = proc.communicate(input="sel;", timeout=self.TIMEOUT) self.assertIn(self.MEMORY_DB_MSG, err) self.assertIn("OperationalError (SQLITE_ERROR)", err) - self.expect_success(proc, err) + self.expect_success(proc) def test_interact_on_disk_file(self): with self.start_cli(TESTFN) as proc: @@ -142,13 +142,13 @@ def test_interact_on_disk_file(self): timeout=self.TIMEOUT) self.assertIn(TESTFN, err) self.assertIn(self.PS1, out) - self.expect_success(proc, err) + self.expect_success(proc) self.addCleanup(unlink, TESTFN) with self.start_cli(TESTFN, "select count(t) from t") as proc: out = proc.stdout.read() err = proc.stderr.read() self.assertIn("(0,)", out) - self.expect_success(proc, err) + self.expect_success(proc) if __name__ == "__main__": From 7ba91e725ec51259ccf3095076692f1171e0b0e1 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Mon, 1 Aug 2022 00:28:43 +0200 Subject: [PATCH 30/33] Use -Xutf8 --- 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 6cdade54b07d6b..b96d0163b92d00 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -14,7 +14,7 @@ class CommandLineInterface(unittest.TestCase): def _do_test(self, *args, expect_success=True): with subprocess.Popen( - [sys.executable, "-m", "sqlite3", *args], + [sys.executable, "-m", "-Xutf8", "sqlite3", *args], encoding="utf-8", bufsize=0, stdout=subprocess.PIPE, @@ -77,7 +77,7 @@ class InteractiveSession(unittest.TestCase): def start_cli(self, *args): return subprocess.Popen( - [sys.executable, "-m", "sqlite3", *args], + [sys.executable, "-Xutf8", "-m", "sqlite3", *args], encoding="utf-8", bufsize=0, stdin=subprocess.PIPE, From b74e010cc538a43669fff73aba6f9d0c53117728 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Mon, 1 Aug 2022 00:47:34 +0200 Subject: [PATCH 31/33] Fix argument order --- 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 b96d0163b92d00..faad4e5bc33f94 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -14,7 +14,7 @@ class CommandLineInterface(unittest.TestCase): def _do_test(self, *args, expect_success=True): with subprocess.Popen( - [sys.executable, "-m", "-Xutf8", "sqlite3", *args], + [sys.executable, "-Xutf8", "-m", "sqlite3", *args], encoding="utf-8", bufsize=0, stdout=subprocess.PIPE, From 903b86712a6ffacac67bc1b9001b5d25d005edb8 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Mon, 1 Aug 2022 09:08:56 +0200 Subject: [PATCH 32/33] Address review: move cleanups up --- 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 faad4e5bc33f94..d374f8ee4fc8d3 100644 --- a/Lib/test/test_sqlite3/test_cli.py +++ b/Lib/test/test_sqlite3/test_cli.py @@ -61,8 +61,8 @@ def test_cli_execute_incomplete_sql(self): self.assertIn("OperationalError (SQLITE_ERROR)", stderr) def test_cli_on_disk_db(self): - out = self.expect_success(TESTFN, "create table t(t)") self.addCleanup(unlink, TESTFN) + out = self.expect_success(TESTFN, "create table t(t)") self.assertEqual(out, "") out = self.expect_success(TESTFN, "select count(t) from t") self.assertIn("(0,)", out) @@ -137,13 +137,13 @@ def test_interact_invalid_sql(self): self.expect_success(proc) def test_interact_on_disk_file(self): + self.addCleanup(unlink, TESTFN) with self.start_cli(TESTFN) as proc: out, err = proc.communicate(input="create table t(t);", timeout=self.TIMEOUT) self.assertIn(TESTFN, err) self.assertIn(self.PS1, out) self.expect_success(proc) - self.addCleanup(unlink, TESTFN) with self.start_cli(TESTFN, "select count(t) from t") as proc: out = proc.stdout.read() err = proc.stderr.read() From 796ba16b6ab164794fc650ffa008aa4f093a23dd Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Mon, 1 Aug 2022 09:27:57 +0200 Subject: [PATCH 33/33] Last adjustment: inline runsql() --- Lib/sqlite3/__main__.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index b416de209d7de7..c62fad84e74bb8 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -27,12 +27,6 @@ def __init__(self, connection): self._con = connection self._cur = connection.cursor() - def runsql(self, sql): - if not sqlite3.complete_statement(sql): - return True - execute(self._cur, sql) - return False - def runsource(self, source, filename="", symbol="single"): match source: case ".version": @@ -42,7 +36,9 @@ def runsource(self, source, filename="", symbol="single"): case ".quit": sys.exit(0) case _: - return self.runsql(source) + if not sqlite3.complete_statement(source): + return True + execute(self._cur, source) return False