-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
gh-133934: Improve .help
in the sqlite3
CLI
#133935
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -41,41 +41,206 @@ def execute(c, sql, suppress_errors=True, theme=theme_no_color): | |||||
|
||||||
class SqliteInteractiveConsole(InteractiveConsole): | ||||||
"""A simple SQLite REPL.""" | ||||||
PS1 = "sqlite> " | ||||||
PS2 = " ... " | ||||||
ruler = "=" | ||||||
doc_header = "Documented commands (type .help <command>):" | ||||||
undoc_header = "Undocumented commands:" | ||||||
|
||||||
def __init__(self, connection, use_color=False): | ||||||
super().__init__() | ||||||
self._con = connection | ||||||
self._cur = connection.cursor() | ||||||
self._use_color = use_color | ||||||
self._theme = get_theme(force_no_color=not use_color) | ||||||
|
||||||
s = self._theme.syntax | ||||||
sys.ps1 = f"{s.prompt}{self.PS1}{s.reset}" | ||||||
sys.ps2 = f"{s.prompt}{self.PS2}{s.reset}" | ||||||
|
||||||
def do_version(self, _): | ||||||
""".version | ||||||
|
||||||
Show version of the runtime SQLite library. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This is the more common way of describing this. And also this does not need a new function. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just as explained at #133935 (comment), we do need a function here. |
||||||
""" | ||||||
print(sqlite3.sqlite_version) | ||||||
|
||||||
def do_help(self, arg): | ||||||
picnixz marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
""".help [-all] [command] | ||||||
|
||||||
Without argument, print the list of available commands. | ||||||
With a command name as argument, print help about that command. | ||||||
With more command names as arguments, only the first one is used. | ||||||
""" | ||||||
if not arg: | ||||||
cmds = sorted(name[3:] for name in dir(self.__class__) | ||||||
if name.startswith("do_")) | ||||||
cmds_doc = [] | ||||||
cmds_undoc = [] | ||||||
for cmd in cmds: | ||||||
if getattr(self, f"do_{cmd}").__doc__: | ||||||
cmds_doc.append(cmd) | ||||||
else: | ||||||
cmds_undoc.append(cmd) | ||||||
self._print_commands(self.doc_header, cmds_doc, 80) | ||||||
self._print_commands(self.undoc_header, cmds_undoc, 80) | ||||||
else: | ||||||
arg = arg.split()[0] | ||||||
if arg in ("-all", "--all"): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should not have both. Please only keep the |
||||||
names = sorted(name for name in dir(self.__class__) | ||||||
if name.startswith("do_")) | ||||||
print(self._help_message_from_method_names(names)) | ||||||
else: | ||||||
if (method := getattr(self, "do_" + arg, None)) is not None: | ||||||
print(self._help_message_from_doc(method.__doc__)) | ||||||
else: | ||||||
self._error(f"No help for '{arg}'") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just use print and add the theme normally IMO |
||||||
|
||||||
def do_quit(self, _): | ||||||
""".q(uit) | ||||||
|
||||||
Exit this program. | ||||||
""" | ||||||
sys.exit(0) | ||||||
Comment on lines
+99
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although it's just a one-liner we still need to put it in a function with a |
||||||
|
||||||
do_q = do_quit | ||||||
|
||||||
def _help_message_from_doc(self, doc): | ||||||
# copied from Lib/pdb.py#L2544 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will change, we don't need it anyway for inter-stdlib copies |
||||||
lines = [line.strip() for line in doc.rstrip().splitlines()] | ||||||
if not lines: | ||||||
return "No help message found." | ||||||
if "" in lines: | ||||||
usage_end = lines.index("") | ||||||
else: | ||||||
usage_end = 1 | ||||||
formatted = [] | ||||||
indent = " " * len(self.PS1) | ||||||
for i, line in enumerate(lines): | ||||||
if i == 0: | ||||||
prefix = "Usage: " | ||||||
elif i < usage_end: | ||||||
prefix = " " | ||||||
else: | ||||||
prefix = "" | ||||||
formatted.append(indent + prefix + line) | ||||||
return "\n".join(formatted) | ||||||
|
||||||
def _help_message_from_method_names(self, names): | ||||||
formatted = [] | ||||||
indent = " " * len(self.PS1) | ||||||
for name in names: | ||||||
if not (doc := getattr(self, name).__doc__): | ||||||
formatted.append(f".{name[3:]}") | ||||||
continue | ||||||
lines = [line.strip() for line in doc.rstrip().splitlines()] | ||||||
if "" in lines: | ||||||
usage_end = lines.index("") | ||||||
else: | ||||||
usage_end = 1 | ||||||
for i, line in enumerate(lines): | ||||||
# skip method aliases, e.g. do_q for do_quit | ||||||
if i == 0 and line in formatted: | ||||||
break | ||||||
elif i < usage_end: | ||||||
formatted.append(line) | ||||||
elif not line and i == usage_end: | ||||||
continue | ||||||
else: | ||||||
formatted.append(indent + line) | ||||||
return "\n".join(formatted) | ||||||
|
||||||
def _error(self, msg): | ||||||
t = self._theme.traceback | ||||||
self.write(f"{t.message}{msg}{t.reset}\n") | ||||||
|
||||||
def _print_commands(self, header, cmds, maxcol): | ||||||
# copied and modified from Lib/cmd.py#L351 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto |
||||||
if cmds: | ||||||
print(header) | ||||||
if self.ruler: | ||||||
print(self.ruler * len(header)) | ||||||
self._columnize(cmds, maxcol-1) | ||||||
print() | ||||||
|
||||||
def _columnize(self, strings, displaywidth=80): | ||||||
"""Display a list of strings as a compact set of columns. | ||||||
|
||||||
Each column is only as wide as necessary. | ||||||
Columns are separated by two spaces (one was not legible enough). | ||||||
""" | ||||||
# copied and modified from Lib/cmd.py#L359 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto |
||||||
if not strings: | ||||||
print("<empty>") | ||||||
return | ||||||
|
||||||
size = len(strings) | ||||||
if size == 1: | ||||||
print(strings[0]) | ||||||
return | ||||||
# Try every row count from 1 upwards | ||||||
for nrows in range(1, size): | ||||||
ncols = (size+nrows-1) // nrows | ||||||
colwidths = [] | ||||||
totwidth = -2 | ||||||
for col in range(ncols): | ||||||
colwidth = 0 | ||||||
for row in range(nrows): | ||||||
i = row + nrows*col | ||||||
if i >= size: | ||||||
break | ||||||
x = strings[i] | ||||||
colwidth = max(colwidth, len(x)) | ||||||
colwidths.append(colwidth) | ||||||
totwidth += colwidth + 2 | ||||||
if totwidth > displaywidth: | ||||||
break | ||||||
if totwidth <= displaywidth: | ||||||
break | ||||||
else: | ||||||
nrows = size | ||||||
ncols = 1 | ||||||
colwidths = [0] | ||||||
for row in range(nrows): | ||||||
texts = [] | ||||||
for col in range(ncols): | ||||||
i = row + nrows*col | ||||||
if i >= size: | ||||||
x = "" | ||||||
else: | ||||||
x = strings[i] | ||||||
texts.append(x) | ||||||
while texts and not texts[-1]: | ||||||
del texts[-1] | ||||||
for col in range(len(texts)): | ||||||
texts[col] = texts[col].ljust(colwidths[col]) | ||||||
print(" ".join(texts)) | ||||||
|
||||||
def runsource(self, source, filename="<input>", symbol="single"): | ||||||
"""Override runsource, the core of the InteractiveConsole REPL. | ||||||
|
||||||
Return True if more input is needed; buffering is done automatically. | ||||||
Return False if input is a complete statement ready for execution. | ||||||
""" | ||||||
theme = get_theme(force_no_color=not self._use_color) | ||||||
|
||||||
if not source or source.isspace(): | ||||||
return False | ||||||
if source[0] == ".": | ||||||
match source[1:].strip(): | ||||||
case "version": | ||||||
print(f"{sqlite3.sqlite_version}") | ||||||
case "help": | ||||||
print("Enter SQL code and press enter.") | ||||||
case "quit": | ||||||
sys.exit(0) | ||||||
case "": | ||||||
pass | ||||||
case _ as unknown: | ||||||
t = theme.traceback | ||||||
if line := source[1:].strip(): | ||||||
try: | ||||||
cmd, arg = line.split(maxsplit=1) | ||||||
except ValueError: | ||||||
cmd, arg = line, None | ||||||
if (func := getattr(self, "do_" + cmd, None)) is not None: | ||||||
func(arg) | ||||||
else: | ||||||
t = self._theme.traceback | ||||||
self.write(f'{t.type}Error{t.reset}:{t.message} unknown' | ||||||
f'command or invalid arguments: "{unknown}".\n{t.reset}') | ||||||
f' command or invalid arguments: "{line}".' | ||||||
f' Enter ".help" for help{t.reset}\n') | ||||||
else: | ||||||
if not sqlite3.complete_statement(source): | ||||||
return True | ||||||
execute(self._cur, source, theme=theme) | ||||||
execute(self._cur, source, theme=self._theme) | ||||||
return False | ||||||
|
||||||
|
||||||
|
@@ -124,10 +289,6 @@ def main(*args): | |||||
""").strip() | ||||||
|
||||||
theme = get_theme() | ||||||
s = theme.syntax | ||||||
|
||||||
sys.ps1 = f"{s.prompt}sqlite> {s.reset}" | ||||||
sys.ps2 = f"{s.prompt} ... {s.reset}" | ||||||
|
||||||
con = sqlite3.connect(args.filename, isolation_level=None) | ||||||
try: | ||||||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,2 @@ | ||||||||||
Make ``.help`` in the :mod:`sqlite3` command-line interface print list of | ||||||||||
available commands and ``.help <command>`` prints help for that command. | ||||||||||
Comment on lines
+1
to
+2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove these and use
sys.ps1
like before, this would break current behavior.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
self.PS1
is needed by this line to get the length of plain prompt string. The length can't be obtained throughsys.ps1
becausesys.ps1
has surrounding control sequences for coloring.I tested it and it doesn't break current behavior.