Skip to content

Commit 833140c

Browse files
StanFromIrelandtanloong
authored andcommitted
cherry pick from and resolve conflicts with 30b1d8f on main branch
(gh-133447: Add basic color to `sqlite3` CLI (#133461))
1 parent 2814fae commit 833140c

File tree

3 files changed

+53
-22
lines changed

3 files changed

+53
-22
lines changed

Lib/sqlite3/__main__.py

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
from argparse import ArgumentParser
1111
from code import InteractiveConsole
1212
from textwrap import dedent
13+
from _colorize import get_theme, theme_no_color
1314

1415
from ._completer import enable_completer
1516

1617

17-
def execute(c, sql, suppress_errors=True):
18+
def execute(c, sql, suppress_errors=True, theme=theme_no_color):
1819
"""Helper that wraps execution of SQL code.
1920
2021
This is used both by the REPL and by direct execution from the CLI.
@@ -27,41 +28,56 @@ def execute(c, sql, suppress_errors=True):
2728
for row in c.execute(sql):
2829
print(row)
2930
except sqlite3.Error as e:
31+
t = theme.traceback
3032
tp = type(e).__name__
3133
try:
32-
print(f"{tp} ({e.sqlite_errorname}): {e}", file=sys.stderr)
34+
tp += f" ({e.sqlite_errorname})"
3335
except AttributeError:
34-
print(f"{tp}: {e}", file=sys.stderr)
36+
pass
37+
print(
38+
f"{t.type}{tp}{t.reset}: {t.message}{e}{t.reset}", file=sys.stderr
39+
)
3540
if not suppress_errors:
3641
sys.exit(1)
3742

3843

3944
class SqliteInteractiveConsole(InteractiveConsole):
4045
"""A simple SQLite REPL."""
4146

42-
def __init__(self, connection):
47+
def __init__(self, connection, use_color=False):
4348
super().__init__()
4449
self._con = connection
4550
self._cur = connection.cursor()
51+
self._use_color = use_color
4652

4753
def runsource(self, source, filename="<input>", symbol="single"):
4854
"""Override runsource, the core of the InteractiveConsole REPL.
4955
5056
Return True if more input is needed; buffering is done automatically.
5157
Return False if input is a complete statement ready for execution.
5258
"""
53-
match source:
54-
case ".version":
55-
print(f"{sqlite3.sqlite_version}")
56-
case ".help":
57-
print("Enter SQL code and press enter.")
58-
case ".quit":
59-
sys.exit(0)
60-
case _:
61-
if not sqlite3.complete_statement(source):
62-
return True
63-
execute(self._cur, source)
64-
return False
59+
theme = get_theme(force_no_color=not self._use_color)
60+
61+
if not source or source.isspace():
62+
return False
63+
if source[0] == ".":
64+
match source[1:].strip():
65+
case "version":
66+
print(f"{sqlite3.sqlite_version}")
67+
case "help":
68+
print("Enter SQL code and press enter.")
69+
case "quit":
70+
sys.exit(0)
71+
case "":
72+
pass
73+
case _ as unknown:
74+
t = theme.traceback
75+
self.write(f'{t.type}Error{t.reset}:{t.message} unknown'
76+
f'command or invalid arguments: "{unknown}".\n{t.reset}')
77+
else:
78+
if not sqlite3.complete_statement(source):
79+
return True
80+
execute(self._cur, source, theme=theme)
6581

6682

6783
def main(*args):
@@ -107,18 +123,22 @@ def main(*args):
107123
Each command will be run using execute() on the cursor.
108124
Type ".help" for more information; type ".quit" or {eofkey} to quit.
109125
""").strip()
110-
sys.ps1 = "sqlite> "
111-
sys.ps2 = " ... "
126+
127+
theme = get_theme()
128+
s = theme.syntax
129+
130+
sys.ps1 = f"{s.prompt}sqlite> {s.reset}"
131+
sys.ps2 = f"{s.prompt} ... {s.reset}"
112132

113133
con = sqlite3.connect(args.filename, isolation_level=None)
114134
try:
115135
if args.sql:
116136
# SQL statement provided on the command-line; execute it directly.
117-
execute(con, args.sql, suppress_errors=False)
137+
execute(con, args.sql, suppress_errors=False, theme=theme)
118138
else:
119139
# No SQL provided; start the REPL.
120140
with enable_completer():
121-
console = SqliteInteractiveConsole(con)
141+
console = SqliteInteractiveConsole(con, use_color=True)
122142
console.interact(banner, exitmsg="")
123143
finally:
124144
con.close()

Lib/test/test_sqlite3/test_cli.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
captured_stdout,
1212
captured_stderr,
1313
captured_stdin,
14-
force_not_colorized,
14+
force_not_colorized_test_class,
1515
requires_subprocess,
1616
)
1717

18+
19+
@force_not_colorized_test_class
1820
class CommandLineInterface(unittest.TestCase):
1921

2022
def _do_test(self, *args, expect_success=True):
@@ -40,7 +42,6 @@ def expect_failure(self, *args):
4042
self.assertEqual(out, "")
4143
return err
4244

43-
@force_not_colorized
4445
def test_cli_help(self):
4546
out = self.expect_success("-h")
4647
self.assertIn("usage: ", out)
@@ -72,6 +73,7 @@ def test_cli_on_disk_db(self):
7273
self.assertIn("(0,)", out)
7374

7475

76+
@force_not_colorized_test_class
7577
class InteractiveSession(unittest.TestCase):
7678
MEMORY_DB_MSG = "Connected to a transient in-memory database"
7779
PS1 = "sqlite> "
@@ -161,6 +163,14 @@ def test_interact_on_disk_file(self):
161163
out, _ = self.run_cli(TESTFN, commands=("SELECT count(t) FROM t;",))
162164
self.assertIn("(0,)\n", out)
163165

166+
def test_color(self):
167+
with unittest.mock.patch("_colorize.can_colorize", return_value=True):
168+
out, err = self.run_cli(commands="TEXT\n")
169+
self.assertIn("\x1b[1;35msqlite> \x1b[0m", out)
170+
self.assertIn("\x1b[1;35m ... \x1b[0m\x1b", out)
171+
out, err = self.run_cli(commands=("sel;",))
172+
self.assertIn('\x1b[1;35mOperationalError (SQLITE_ERROR)\x1b[0m: '
173+
'\x1b[35mnear "sel": syntax error\x1b[0m', err)
164174

165175
@requires_subprocess()
166176
class Completer(unittest.TestCase):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add basic color to :mod:`sqlite3` CLI interface.

0 commit comments

Comments
 (0)