Skip to content

Commit b0afaad

Browse files
loic-simonambv
authored andcommitted
PyREPL completion: insert "import" after "from foo "
Improve _pyrepl._module_completer.ModuleCompleter.get_completions: * "from x.y.z " [tab]-> "from x.y.z import " * "from foo" [tab]-> "from foo " [tab]-> "from foo import" (if only one suggestion)
1 parent 39bc958 commit b0afaad

3 files changed

Lines changed: 90 additions & 70 deletions

File tree

‎Lib/_pyrepl/_module_completer.py‎

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def complete(
9393
self,
9494
from_name: str | None,
9595
name: str | None,
96+
space_end: bool,
9697
*,
9798
include_values: bool = True,
9899
) -> tuple[list[str], list[Any], CompletionAction | None]:
@@ -107,10 +108,16 @@ def complete(
107108
return names, values, None
108109

109110
if name is None:
111+
if space_end and from_name:
112+
# from x.y.z <tab>
113+
return ["import "], [sys] if include_values else [], None
110114
# from x.y.z<tab>
111115
path, prefix = self.get_path_and_prefix(from_name)
112116
modules = self.find_modules(path, prefix)
113117
names = [self.format_completion(path, module) for module in modules]
118+
if len(names) == 1:
119+
# One match: insert a space to allow for "import" suggestion
120+
names[0] = f"{names[0]} "
114121
# These are always modules, use dummy values to get the right color
115122
values = [sys] * len(names) if include_values else []
116123
return names, values, None
@@ -343,11 +350,12 @@ class ImportParser:
343350
suitable for autocomplete suggestions.
344351
345352
Examples:
346-
- import foo -> Result(from_name=None, name='foo')
347-
- import foo. -> Result(from_name=None, name='foo.')
348-
- from foo -> Result(from_name='foo', name=None)
349-
- from foo import bar -> Result(from_name='foo', name='bar')
350-
- from .foo import ( -> Result(from_name='.foo', name='')
353+
- `import foo` -> Result(from_name=None, name='foo')
354+
- `import foo.` -> Result(from_name=None, name='foo.')
355+
- `from foo` -> Result(from_name='foo', name=None)
356+
- `from foo ` -> Result(from_name='foo', name=None, end_space=True)
357+
- `from foo import bar` -> Result(from_name='foo', name='bar')
358+
- `from .foo import (` -> Result(from_name='.foo', name='')
351359
352360
Note that the parser works in reverse order, starting from the
353361
last token in the input string. This makes the parser more robust
@@ -377,10 +385,10 @@ def __init__(self, code: str) -> None:
377385
tokens = []
378386
self.tokens = TokenQueue(tokens[::-1])
379387

380-
def parse(self) -> tuple[str | None, str | None] | None:
388+
def parse(self) -> tuple[str | None, str | None, bool] | None:
381389
if not (res := self._parse()):
382390
return None
383-
return res.from_name, res.name
391+
return res.from_name, res.name, res.space_end
384392

385393
def _parse(self) -> Result | None:
386394
with self.tokens.save_state():
@@ -390,7 +398,7 @@ def _parse(self) -> Result | None:
390398

391399
def parse_import(self) -> Result:
392400
if self.code.rstrip().endswith('import') and self.code.endswith(' '):
393-
return Result(name='')
401+
return Result(name='', space_end=True)
394402
if self.tokens.peek_string(','):
395403
name = ''
396404
else:
@@ -403,27 +411,32 @@ def parse_import(self) -> Result:
403411
self.tokens.pop()
404412
self.parse_dotted_as_name()
405413
if self.tokens.peek_string('import'):
406-
return Result(name=name)
414+
return Result(name=name, space_end=self.code.endswith(' '))
407415
raise ParseError('parse_import')
408416

409417
def parse_from_import(self) -> Result:
418+
space_end = self.code.endswith(' ')
410419
stripped = self.code.rstrip()
411-
if stripped.endswith('import') and self.code.endswith(' '):
412-
return Result(from_name=self.parse_empty_from_import(), name='')
413-
if stripped.endswith('from') and self.code.endswith(' '):
414-
return Result(from_name='')
420+
if stripped.endswith('import') and space_end:
421+
from_name = self.parse_empty_from_import()
422+
return Result(from_name=from_name, name='', space_end=space_end)
423+
if stripped.endswith('from') and space_end:
424+
return Result(from_name='', space_end=space_end)
415425
if self.tokens.peek_string('(') or self.tokens.peek_string(','):
416-
return Result(from_name=self.parse_empty_from_import(), name='')
417-
if self.code.endswith(' '):
418-
raise ParseError('parse_from_import')
426+
from_name = self.parse_empty_from_import()
427+
return Result(from_name=from_name, name='', space_end=space_end)
419428
name = self.parse_dotted_name()
420429
if '.' in name:
430+
if name.endswith(".") and space_end:
431+
raise ParseError('parse_from_import')
421432
self.tokens.pop_string('from')
422-
return Result(from_name=name)
433+
return Result(from_name=name, space_end=space_end)
423434
if self.tokens.peek_string('from'):
424-
return Result(from_name=name)
435+
return Result(from_name=name, space_end=space_end)
436+
if space_end:
437+
raise ParseError('parse_from_import')
425438
from_name = self.parse_empty_from_import()
426-
return Result(from_name=from_name, name=name)
439+
return Result(from_name=from_name, name=name, space_end=space_end)
427440

428441
def parse_empty_from_import(self) -> str:
429442
if self.tokens.peek_string(','):
@@ -489,10 +502,11 @@ class ParseError(Exception):
489502
pass
490503

491504

492-
@dataclass(frozen=True)
505+
@dataclass(frozen=True, kw_only=True)
493506
class Result:
494507
from_name: str | None = None
495508
name: str | None = None
509+
space_end: bool
496510

497511

498512
class TokenQueue:

‎Lib/test/test_pyrepl/test_pyrepl.py‎

Lines changed: 54 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,13 +1230,18 @@ def test_completions(self):
12301230
("import importlib.resources.\t\ta\t\n", "import importlib.resources.abc"),
12311231
("import foo, impo\t\n", "import foo, importlib"),
12321232
("import foo as bar, impo\t\n", "import foo as bar, importlib"),
1233-
("from impo\t\n", "from importlib"),
1234-
("from importlib.res\t\n", "from importlib.resources"),
1235-
("from importlib.\t\tres\t\n", "from importlib.resources"),
1236-
("from importlib.resources.ab\t\n", "from importlib.resources.abc"),
1233+
("from impo\t\n", "from importlib "),
1234+
("from impo\t\t\n", "from importlib import "),
1235+
("from impo \t\n", "from impo import "),
1236+
("from importlib.res\t\n", "from importlib.resources "),
1237+
("from importlib.\t\tres\t\n", "from importlib.resources "),
1238+
("from importlib.res\t\t\n", "from importlib.resources import "),
1239+
("from importlib.res \t\n", "from importlib.res import "),
1240+
("from importlib.resources.ab\t\n", "from importlib.resources.abc "),
12371241
("from importlib import mac\t\n", "from importlib import machinery"),
12381242
("from importlib import res\t\n", "from importlib import resources"),
1239-
("from importlib.res\t import a\t\n", "from importlib.resources import abc"),
1243+
("from importlib.res\timport a\t\n", "from importlib.resources import abc"),
1244+
("from importlib.res\t\ta\t\n", "from importlib.resources import abc"),
12401245
("from __phello__ import s\t\n", "from __phello__ import spam"), # frozen module
12411246
)
12421247
for code, expected in cases:
@@ -1253,10 +1258,10 @@ def test_private_completions(self):
12531258
cases = (
12541259
# Return public methods by default
12551260
("import \t\n", "import public"),
1256-
("from \t\n", "from public"),
1261+
("from \t\n", "from public "),
12571262
# Return private methods if explicitly specified
12581263
("import _\t\n", "import _private"),
1259-
("from _\t\n", "from _private"),
1264+
("from _\t\n", "from _private "),
12601265
)
12611266
for code, expected in cases:
12621267
with self.subTest(code=code):
@@ -1289,7 +1294,7 @@ def test_sub_module_private_completions(self):
12891294
def test_builtin_completion_top_level(self):
12901295
cases = (
12911296
("import bui\t\n", "import builtins"),
1292-
("from bui\t\n", "from builtins"),
1297+
("from bui\t\n", "from builtins "),
12931298
)
12941299
for code, expected in cases:
12951300
with self.subTest(code=code):
@@ -1302,11 +1307,11 @@ def test_relative_completions(self):
13021307
cases = (
13031308
(None, "from .readl\t\n", "from .readl"),
13041309
(None, "from . import readl\t\n", "from . import readl"),
1305-
("_pyrepl", "from .readl\t\n", "from .readline"),
1310+
("_pyrepl", "from .readl\t\n", "from .readline "),
13061311
("_pyrepl", "from . import readl\t\n", "from . import readline"),
13071312
("_pyrepl", "from .readline import mul\t\n", "from .readline import multiline_input"),
13081313
("_pyrepl", "from .. import toodeep\t\n", "from .. import toodeep"),
1309-
("concurrent", "from .futures.i\t\n", "from .futures.interpreter"),
1314+
("concurrent", "from .futures.i\t\n", "from .futures.interpreter "),
13101315
)
13111316
for package, code, expected in cases:
13121317
with self.subTest(code=code):
@@ -1610,41 +1615,43 @@ def test_get_path_and_prefix(self):
16101615

16111616
def test_parse(self):
16121617
cases = (
1613-
('import ', (None, '')),
1614-
('import foo', (None, 'foo')),
1615-
('import foo,', (None, '')),
1616-
('import foo, ', (None, '')),
1617-
('import foo, bar', (None, 'bar')),
1618-
('import foo, bar, baz', (None, 'baz')),
1619-
('import foo as bar,', (None, '')),
1620-
('import foo as bar, ', (None, '')),
1621-
('import foo as bar, baz', (None, 'baz')),
1622-
('import a.', (None, 'a.')),
1623-
('import a.b', (None, 'a.b')),
1624-
('import a.b.', (None, 'a.b.')),
1625-
('import a.b.c', (None, 'a.b.c')),
1626-
('import a.b.c, foo', (None, 'foo')),
1627-
('import a.b.c, foo.bar', (None, 'foo.bar')),
1628-
('import a.b.c, foo.bar,', (None, '')),
1629-
('import a.b.c, foo.bar, ', (None, '')),
1630-
('from foo', ('foo', None)),
1631-
('from a.', ('a.', None)),
1632-
('from a.b', ('a.b', None)),
1633-
('from a.b.', ('a.b.', None)),
1634-
('from a.b.c', ('a.b.c', None)),
1635-
('from foo import ', ('foo', '')),
1636-
('from foo import a', ('foo', 'a')),
1637-
('from ', ('', None)),
1638-
('from . import a', ('.', 'a')),
1639-
('from .foo import a', ('.foo', 'a')),
1640-
('from ..foo import a', ('..foo', 'a')),
1641-
('from foo import (', ('foo', '')),
1642-
('from foo import ( ', ('foo', '')),
1643-
('from foo import (a', ('foo', 'a')),
1644-
('from foo import (a,', ('foo', '')),
1645-
('from foo import (a, ', ('foo', '')),
1646-
('from foo import (a, c', ('foo', 'c')),
1647-
('from foo import (a as b, c', ('foo', 'c')),
1618+
('import ', (None, '', True)),
1619+
('import foo', (None, 'foo', False)),
1620+
('import foo,', (None, '', False)),
1621+
('import foo, ', (None, '', True)),
1622+
('import foo, bar', (None, 'bar', False)),
1623+
('import foo, bar, baz', (None, 'baz', False)),
1624+
('import foo as bar,', (None, '', False)),
1625+
('import foo as bar, ', (None, '', True)),
1626+
('import foo as bar, baz', (None, 'baz', False)),
1627+
('import a.', (None, 'a.', False)),
1628+
('import a.b', (None, 'a.b', False)),
1629+
('import a.b.', (None, 'a.b.', False)),
1630+
('import a.b.c', (None, 'a.b.c', False)),
1631+
('import a.b.c, foo', (None, 'foo', False)),
1632+
('import a.b.c, foo.bar', (None, 'foo.bar', False)),
1633+
('import a.b.c, foo.bar,', (None, '', False)),
1634+
('import a.b.c, foo.bar, ', (None, '', True)),
1635+
('from foo', ('foo', None, False)),
1636+
('from foo ', ('foo', None, True)),
1637+
('from a.', ('a.', None, False)),
1638+
('from a.b', ('a.b', None, False)),
1639+
('from a.b.', ('a.b.', None, False)),
1640+
('from a.b ', ('a.b', None, True)),
1641+
('from a.b.c', ('a.b.c', None, False)),
1642+
('from foo import ', ('foo', '', True)),
1643+
('from foo import a', ('foo', 'a', False)),
1644+
('from ', ('', None, True)),
1645+
('from . import a', ('.', 'a', False)),
1646+
('from .foo import a', ('.foo', 'a', False)),
1647+
('from ..foo import a', ('..foo', 'a', False)),
1648+
('from foo import (', ('foo', '', False)),
1649+
('from foo import ( ', ('foo', '', True)),
1650+
('from foo import (a', ('foo', 'a', False)),
1651+
('from foo import (a,', ('foo', '', False)),
1652+
('from foo import (a, ', ('foo', '', True)),
1653+
('from foo import (a, c', ('foo', 'c', False)),
1654+
('from foo import (a as b, c', ('foo', 'c', False)),
16481655
)
16491656
for code, parsed in cases:
16501657
parser = ImportParser(code)
@@ -1668,12 +1675,9 @@ def test_parse_error(self):
16681675
cases = (
16691676
'',
16701677
'import foo ',
1671-
'from foo ',
16721678
'import foo. ',
16731679
'import foo.bar ',
1674-
'from foo ',
16751680
'from foo. ',
1676-
'from foo.bar ',
16771681
'from foo import bar ',
16781682
'from foo import (bar ',
16791683
'from foo import bar, baz ',
@@ -1712,9 +1716,9 @@ def test_parse_error(self):
17121716
'if 1:\n pass\n\tpass', # _tokenize TabError -> tokenize TabError
17131717
)
17141718
for code in cases:
1715-
parser = ImportParser(code)
1716-
actual = parser.parse()
17171719
with self.subTest(code=code):
1720+
parser = ImportParser(code)
1721+
actual = parser.parse()
17181722
self.assertEqual(actual, None)
17191723

17201724
@patch.dict(sys.modules)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Insert ``import`` after ``from x.y.z <tab>`` in the :term:`REPL`
2+
auto-completion.

0 commit comments

Comments
 (0)