-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
gh-133390: Support basic completion for sqlite3 command-line interface #133393
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?
Conversation
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
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.
This needs a whatsnew entry, (maybe a note in the docs?), a NEWS entry and tests.
This is quite limited completion so I guess it could work.
I think we need a separate nodule for this because if we want to have a smarter completion later the code is better to be isolated, so something like sqlite3._completer. If possible, can we have an autogenerated list of keywords? and those keywords could also be stored in a (private) module-level list. |
Misc/NEWS.d/next/Library/2025-05-05-03-14-08.gh-issue-133390.AuTggn.rst
Outdated
Show resolved
Hide resolved
Please add tests |
Yes, tests is coming. I will try to find a way to autogenerate list of keywords. |
You can mark the pr as a draft. |
782a599
to
5005d85
Compare
Added tests and moved keyword list to module level. Keywords seems not easy to auto-generate, the SQLite document says they can be accessed by |
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.
With #133447 in mind the keywords list would be quite handy (I think hardcoding is fine, it doesn't change often anyway). Maybe it could be moved elsewhere, what do you think Benedikt?
15019ea
to
22652ae
Compare
Hi @picnixz, as Stan mentioned, beta freeze is around the corner, but I really hope this can be shipped in 3.14, do you think there is a chance to merge it before beta freeze? |
Please, do not force push. See https://devguide.python.org/getting-started/pull-request-lifecycle/. |
Lib/sqlite3/_completer.py
Outdated
from contextlib import contextmanager | ||
|
||
|
||
_keywords = ("ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", |
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.
We're anyway in a private module, so name this variable KEYWORDS
. Ideally, I really want it coming from C but we can do it in a follow-up PR.
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.
I moved KEYWORDS
to C in the latest commit. I am not confident with my C code and look forward to your review. Thank you very much!
Lib/test/test_sqlite3/test_cli.py
Outdated
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) |
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.
Let's find a way to avoid this. If this is a bug in run_pty
, we should fix it (do we have some FORCE_NO_COLOR
envvar perhaps?)
cc @hugovk as the colorization expert
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.
See: #133393 (comment)
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.
I figure it out. run_pty()
has no bug. The problem is with GNU Readline, which needs special settings to turn off color.
Using env={"NO_COLOR":"1"}
in subprocess does work, but it only stops coloring within what Python can control, like the sqlite>
prompt. To disable color in GNU Readline itself, we need to run readline.parse_and_bind("set colored-completion-prefix off")
.
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.
Oh interesting. Maybe that's something to consider when we add colors?
cc @hugovk
I have pushed the update.
Sorry, I will pay attention to this from now on. |
def setUpClass(cls): | ||
readline = import_module("readline") | ||
if readline.backend == "editline": | ||
raise unittest.SkipTest("libedit readline is not supported") |
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.
raise unittest.SkipTest("libedit readline is not supported") | |
self.unittest.SkipTest("libedit readline is not supported") |
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.
You mean unittest.TestCase.skipTest()
. There are many uses of it in the codebase, but there are also quite a few uses of raise unittest.SkipTest()
, for example in test_pdb.py#L4759 which is used under exactly the same occasion (I actually copied that block from pdb to here). I think raising is OK?
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.
It's incorrect to use self.skipTest
. We're in a class method so we can only raise the exception. skipTest
is a regular method, not a class method.
Lib/test/test_sqlite3/test_cli.py
Outdated
raise unittest.SkipTest("libedit readline is not supported") | ||
|
||
def test_keyword_completion(self): | ||
script = "from sqlite3.__main__ import main; main()" |
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.
Why not just use run_cli from the interactiveconsole tests above?
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.
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.
Then where are the completion candidates sent? The commands should be executed.
I've opened tanloong#1 instead of more suggestions here. It greatly simplifies your tests. |
…d back" This reverts commit 9d03730.
"NO_COLOR" only affects color for the `sqlite>` prompt, it does not help for disabling color of completion prefix.
4-space indents; 79 line width; outermost curly braces in column 1 in function definition; blank line after local variable declaration;
The failed test is unrelated to this PR. |
sqlite3 | ||
------- | ||
|
||
* Support keyword completion in the :mod:`sqlite3` command-line interface. | ||
(Contributed by Tan Long in :gh:`133393`.) |
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.
Let's move this under the "Improved modules" section instead of "New features". The "New features" is reserved for PEP-sized items.
if state == 0: | ||
_completion_matches = [c + " " for c in SQLITE_KEYWORDS if c.startswith(text.upper())] |
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.
if state == 0: | |
_completion_matches = [c + " " for c in SQLITE_KEYWORDS if c.startswith(text.upper())] | |
if state == 0: | |
text_upper = text.upper() | |
_completion_matches = [c for c in SQLITE_KEYWORDS if c.startswith(text_upper)] |
Otherwise, I think text.upper()
is going to be called for each c
. Then you can do return _completion_matches[state] + " "
instead (no need to store the extra whitespace) as it would create a new string while here you can just create a new reference to an existing string.
@@ -200,5 +205,74 @@ def test_color(self): | |||
self.assertIn('\x1b[1;35mOperationalError (SQLITE_ERROR)\x1b[0m: ' | |||
'\x1b[35mnear "sel": syntax error\x1b[0m', err) | |||
|
|||
@requires_subprocess() |
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.
@requires_subprocess() | |
@requires_subprocess() |
Two blank lines to separate classes.
def setUpClass(cls): | ||
readline = import_module("readline") | ||
if readline.backend == "editline": | ||
raise unittest.SkipTest("libedit readline is not supported") |
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.
It's incorrect to use self.skipTest
. We're in a class method so we can only raise the exception. skipTest
is a regular method, not a class method.
script = textwrap.dedent(""" | ||
import readline | ||
readline.parse_and_bind("set colored-completion-prefix off") | ||
from sqlite3.__main__ import main; main() | ||
""") |
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.
You can have a helper method:
def write_input(self, input):
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)
output = run_pty(script, input, env={"NO_COLOR": "1"}) | ||
output_lines = output.decode().splitlines() | ||
candidates = [] | ||
for line in output_lines[-2::-1]: |
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.
Why are we iterating in reverse order and then sorting the candidates in reverse order as well?
if line.startswith(self.PS1): | ||
break | ||
candidates.append(line.strip()) | ||
self.assertEqual(sorted(candidates, reverse=True), candidates) |
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.
I'm confused here. Are we checking that candidates are already sorted? or maybe you wanted to check against SQLITE_KEYWORDS
?
import unittest | ||
|
||
from _sqlite3 import SQLITE_KEYWORDS |
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.
When are we using it?
|
||
if __name__ == "__main__": |
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.
if __name__ == "__main__": | |
if __name__ == "__main__": |
add_sequence_constants(PyObject *module) | ||
{ | ||
PyObject *kwd; | ||
const char *_keywords[] = { |
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.
When I said "we need it in C", I meant, we need the list of keywords given by sqlite3 directly, not by hardcoding it in C. Hardcoding it in C doesn't help.
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
This adds tab-completion for the 147 SQLite Keywords. A whitespace is appended to completion candidates for users' convenience and to mimic the behavior of sqlite3 tool.