Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit6767d02

Browse files
encukoutanloong
authored andcommitted
pythongh-133390: Support SQL keyword completion for sqlite3 CLI (pythonGH-133393) (pythonGH-135292)
Co-authored-by: Tan Long <tanloong@foxmail.com>
1 parentc4d421c commit6767d02

File tree

7 files changed

+206
-6
lines changed

7 files changed

+206
-6
lines changed

‎Doc/whatsnew/3.15.rst‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ shelve
134134
(Contributed by Andrea Oliveri in:gh:`134004`.)
135135

136136

137+
sqlite3
138+
-------
139+
140+
* Support SQL keyword completion in the:mod:`sqlite3` command-line interface.
141+
(Contributed by Long Tan in:gh:`133393`.)
142+
143+
137144
ssl
138145
---
139146

‎Lib/sqlite3/__main__.py‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
fromtextwrapimportdedent
1313
from_colorizeimportget_theme,theme_no_color
1414

15+
from ._completerimportcompleter
16+
1517

1618
defexecute(c,sql,suppress_errors=True,theme=theme_no_color):
1719
"""Helper that wraps execution of SQL code.
@@ -136,12 +138,9 @@ def main(*args):
136138
execute(con,args.sql,suppress_errors=False,theme=theme)
137139
else:
138140
# No SQL provided; start the REPL.
139-
console=SqliteInteractiveConsole(con,use_color=True)
140-
try:
141-
importreadline# noqa: F401
142-
exceptImportError:
143-
pass
144-
console.interact(banner,exitmsg="")
141+
withcompleter():
142+
console=SqliteInteractiveConsole(con,use_color=True)
143+
console.interact(banner,exitmsg="")
145144
finally:
146145
con.close()
147146

‎Lib/sqlite3/_completer.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
fromcontextlibimportcontextmanager
2+
3+
try:
4+
from_sqlite3importSQLITE_KEYWORDS
5+
exceptImportError:
6+
SQLITE_KEYWORDS= ()
7+
8+
_completion_matches= []
9+
10+
11+
def_complete(text,state):
12+
global_completion_matches
13+
14+
ifstate==0:
15+
text_upper=text.upper()
16+
_completion_matches= [cforcinSQLITE_KEYWORDSifc.startswith(text_upper)]
17+
try:
18+
return_completion_matches[state]+" "
19+
exceptIndexError:
20+
returnNone
21+
22+
23+
@contextmanager
24+
defcompleter():
25+
try:
26+
importreadline
27+
exceptImportError:
28+
yield
29+
return
30+
31+
old_completer=readline.get_completer()
32+
try:
33+
readline.set_completer(_complete)
34+
ifreadline.backend=="editline":
35+
# libedit uses "^I" instead of "tab"
36+
command_string="bind ^I rl_complete"
37+
else:
38+
command_string="tab: complete"
39+
readline.parse_and_bind(command_string)
40+
yield
41+
finally:
42+
readline.set_completer(old_completer)

‎Lib/test/test_sqlite3/test_cli.py‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
"""sqlite3 CLI tests."""
22
importsqlite3
3+
importsys
4+
importtextwrap
35
importunittest
6+
importunittest.mock
7+
importos
48

59
fromsqlite3.__main__importmainascli
10+
fromtest.support.import_helperimportimport_module
611
fromtest.support.os_helperimportTESTFN,unlink
12+
fromtest.support.pty_helperimportrun_pty
713
fromtest.supportimport (
814
captured_stdout,
915
captured_stderr,
1016
captured_stdin,
1117
force_not_colorized_test_class,
18+
requires_subprocess,
19+
verbose,
1220
)
1321

1422

@@ -200,5 +208,108 @@ def test_color(self):
200208
self.assertIn('\x1b[1;35mOperationalError (SQLITE_ERROR)\x1b[0m: '
201209
'\x1b[35mnear "sel": syntax error\x1b[0m',err)
202210

211+
212+
@requires_subprocess()
213+
@force_not_colorized_test_class
214+
classCompletion(unittest.TestCase):
215+
PS1="sqlite> "
216+
217+
@classmethod
218+
defsetUpClass(cls):
219+
_sqlite3=import_module("_sqlite3")
220+
ifnothasattr(_sqlite3,"SQLITE_KEYWORDS"):
221+
raiseunittest.SkipTest("unable to determine SQLite keywords")
222+
223+
readline=import_module("readline")
224+
ifreadline.backend=="editline":
225+
raiseunittest.SkipTest("libedit readline is not supported")
226+
227+
defwrite_input(self,input_,env=None):
228+
script=textwrap.dedent("""
229+
import readline
230+
from sqlite3.__main__ import main
231+
232+
readline.parse_and_bind("set colored-completion-prefix off")
233+
main()
234+
""")
235+
returnrun_pty(script,input_,env)
236+
237+
deftest_complete_sql_keywords(self):
238+
# List candidates starting with 'S', there should be multiple matches.
239+
input_=b"S\t\tEL\t 1;\n.quit\n"
240+
output=self.write_input(input_)
241+
self.assertIn(b"SELECT",output)
242+
self.assertIn(b"SET",output)
243+
self.assertIn(b"SAVEPOINT",output)
244+
self.assertIn(b"(1,)",output)
245+
246+
# Keywords are completed in upper case for even lower case user input.
247+
input_=b"sel\t\t 1;\n.quit\n"
248+
output=self.write_input(input_)
249+
self.assertIn(b"SELECT",output)
250+
self.assertIn(b"(1,)",output)
251+
252+
@unittest.skipIf(sys.platform.startswith("freebsd"),
253+
"Two actual tabs are inserted when there are no matching"
254+
" completions in the pseudo-terminal opened by run_pty()"
255+
" on FreeBSD")
256+
deftest_complete_no_match(self):
257+
input_=b"xyzzy\t\t\b\b\b\b\b\b\b.quit\n"
258+
# Set NO_COLOR to disable coloring for self.PS1.
259+
output=self.write_input(input_,env={**os.environ,"NO_COLOR":"1"})
260+
lines=output.decode().splitlines()
261+
indices= (
262+
ifori,lineinenumerate(lines,1)
263+
ifline.startswith(f"{self.PS1}xyzzy")
264+
)
265+
line_num=next(indices,-1)
266+
self.assertNotEqual(line_num,-1)
267+
# Completions occupy lines, assert no extra lines when there is nothing
268+
# to complete.
269+
self.assertEqual(line_num,len(lines))
270+
271+
deftest_complete_no_input(self):
272+
from_sqlite3importSQLITE_KEYWORDS
273+
274+
script=textwrap.dedent("""
275+
import readline
276+
from sqlite3.__main__ import main
277+
278+
# Configure readline to ...:
279+
# - hide control sequences surrounding each candidate
280+
# - hide "Display all xxx possibilities? (y or n)"
281+
# - hide "--More--"
282+
# - show candidates one per line
283+
readline.parse_and_bind("set colored-completion-prefix off")
284+
readline.parse_and_bind("set colored-stats off")
285+
readline.parse_and_bind("set completion-query-items 0")
286+
readline.parse_and_bind("set page-completions off")
287+
readline.parse_and_bind("set completion-display-width 0")
288+
readline.parse_and_bind("set show-all-if-ambiguous off")
289+
readline.parse_and_bind("set show-all-if-unmodified off")
290+
291+
main()
292+
""")
293+
input_=b"\t\t.quit\n"
294+
output=run_pty(script,input_,env={**os.environ,"NO_COLOR":"1"})
295+
try:
296+
lines=output.decode().splitlines()
297+
indices= [
298+
ifori,lineinenumerate(lines)
299+
ifline.startswith(self.PS1)
300+
]
301+
self.assertEqual(len(indices),2)
302+
start,end=indices
303+
candidates= [l.strip()forlinlines[start+1:end]]
304+
self.assertEqual(candidates,sorted(SQLITE_KEYWORDS))
305+
except:
306+
ifverbose:
307+
print(' PTY output: '.center(30,'-'))
308+
print(output.decode(errors='replace'))
309+
print(' end PTY output '.center(30,'-'))
310+
raise
311+
312+
313+
203314
if__name__=="__main__":
204315
unittest.main()

‎Misc/ACKS‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1869,6 +1869,7 @@ Neil Tallim
18691869
Geoff Talvola
18701870
Anish Tambe
18711871
Musashi Tamura
1872+
Long Tan
18721873
William Tanksley
18731874
Christian Tanzer
18741875
Steven Taschuk
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support keyword completion in the:mod:`sqlite3` command-line interface.

‎Modules/_sqlite/module.c‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include"microprotocols.h"
3333
#include"row.h"
3434
#include"blob.h"
35+
#include"util.h"
3536

3637
#ifSQLITE_VERSION_NUMBER<3015002
3738
#error "SQLite 3.15.2 or higher required"
@@ -404,6 +405,40 @@ pysqlite_error_name(int rc)
404405
returnNULL;
405406
}
406407

408+
staticint
409+
add_keyword_tuple(PyObject*module)
410+
{
411+
#ifSQLITE_VERSION_NUMBER >=3024000
412+
intcount=sqlite3_keyword_count();
413+
PyObject*keywords=PyTuple_New(count);
414+
if (keywords==NULL) {
415+
return-1;
416+
}
417+
for (inti=0;i<count;i++) {
418+
constchar*keyword;
419+
intsize;
420+
intresult=sqlite3_keyword_name(i,&keyword,&size);
421+
if (result!=SQLITE_OK) {
422+
pysqlite_state*state=pysqlite_get_state(module);
423+
set_error_from_code(state,result);
424+
gotoerror;
425+
}
426+
PyObject*kwd=PyUnicode_FromStringAndSize(keyword,size);
427+
if (!kwd) {
428+
gotoerror;
429+
}
430+
PyTuple_SET_ITEM(keywords,i,kwd);
431+
}
432+
returnPyModule_Add(module,"SQLITE_KEYWORDS",keywords);
433+
434+
error:
435+
Py_DECREF(keywords);
436+
return-1;
437+
#else
438+
return0;
439+
#endif
440+
}
441+
407442
staticint
408443
add_integer_constants(PyObject*module) {
409444
#defineADD_INT(ival) \
@@ -702,6 +737,10 @@ module_exec(PyObject *module)
702737
gotoerror;
703738
}
704739

740+
if (add_keyword_tuple(module)<0) {
741+
gotoerror;
742+
}
743+
705744
if (PyModule_AddStringConstant(module,"sqlite_version",sqlite3_libversion())) {
706745
gotoerror;
707746
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp