From 8d35dacda3ba3f5b1e5fd803303adc5c5833e0e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Wed, 15 Apr 2026 21:55:48 +0200 Subject: [PATCH 1/2] PyREPL: fix completion inserted if spaces in import statements Fix `from math .` inserting `from math .ath.integer` instead of `from math.integer` --- Lib/_pyrepl/_module_completer.py | 28 +++++++++++++++++-- Lib/test/test_pyrepl/test_pyrepl.py | 11 ++++++++ ...6-04-15-21-55-45.gh-issue-69605.NwoP6l.rst | 2 ++ 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-04-15-21-55-45.gh-issue-69605.NwoP6l.rst diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index a22b0297b24ea0..d3251e4a129be0 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -11,9 +11,9 @@ from io import StringIO from contextlib import contextmanager from dataclasses import dataclass -from itertools import chain +from itertools import chain, takewhile from tokenize import TokenInfo - +from .trace import trace TYPE_CHECKING = False if TYPE_CHECKING: @@ -82,7 +82,9 @@ def get_completions(self, line: str) -> tuple[list[str], CompletionAction | None if not result: return None try: - return self.complete(*result) + names, action = self.complete(*result) + names = [self._remove_separated_words(line, c) for c in names] + return names, action except Exception: # Some unexpected error occurred, make it look like # no completions are available @@ -300,6 +302,26 @@ def _do_import() -> str | None: return (prompt, _do_import) + def _remove_separated_words(self, line: str, completion: str) -> str: + """Remove completion parts imputed as separate words. + + Needed because the completer insert any completion provided + by replacing the stem (eg. word) currently imputed, if any. + + Examples: + - 'import foo.', 'foo.bar' -> 'foo.bar' + - 'import foo .', 'foo.bar' -> '.bar' + - 'from foo import ', 'bar' -> 'bar' + - 'import x.x .x.', 'x.x.x.x' -> '.x.x' + """ + last_word = line.split(" ")[-1] + if not last_word: + return completion + index = completion.rfind(last_word) + if index > 0: + return completion[index:] + return completion + class ImportParser: """ diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 9d0a4ed5316a3f..df82a99522bc52 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1176,6 +1176,12 @@ def test_completions(self): ("from importlib import res\t\n", "from importlib import resources"), ("from importlib.res\t import a\t\n", "from importlib.resources import abc"), ("from __phello__ import s\t\n", "from __phello__ import spam"), # frozen module + ("import importlib .res\t\n", "import importlib .resources"), + ("import importlib. res\t\n", "import importlib. resources"), + ("import importlib . res\t\n", "import importlib . resources"), + ("import importlib .metadata.\t\n", "import importlib .metadata.diagnose"), + ("import importlib .metadata .\t\n", "import importlib .metadata .diagnose"), + ("from importlib .resources .a\t\n", "from importlib .resources .abc"), ) for code, expected in cases: with self.subTest(code=code): @@ -1564,8 +1570,13 @@ def test_parse(self): ('import a.b.c, foo.bar, ', (None, '')), ('from foo', ('foo', None)), ('from a.', ('a.', None)), + ('from a .', ('a.', None)), + ('from a .', ('a.', None)), + ('from a .', ('a.', None)), ('from a.b', ('a.b', None)), ('from a.b.', ('a.b.', None)), + ('from a. b.', ('a.b.', None)), + ('from a . b .', ('a.b.', None)), ('from a.b.c', ('a.b.c', None)), ('from foo import ', ('foo', '')), ('from foo import a', ('foo', 'a')), diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-15-21-55-45.gh-issue-69605.NwoP6l.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-15-21-55-45.gh-issue-69605.NwoP6l.rst new file mode 100644 index 00000000000000..77491c6e5f6207 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-15-21-55-45.gh-issue-69605.NwoP6l.rst @@ -0,0 +1,2 @@ +Fix :term:`REPL` completions inserted incorrectly when there was spaces in +import statements. From 799bd643aea515c83aaf263fac1fd8ce4b51197a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Wed, 15 Apr 2026 22:02:27 +0200 Subject: [PATCH 2/2] Remove unused imports --- Lib/_pyrepl/_module_completer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index d3251e4a129be0..5b07005319dd27 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -11,9 +11,9 @@ from io import StringIO from contextlib import contextmanager from dataclasses import dataclass -from itertools import chain, takewhile +from itertools import chain from tokenize import TokenInfo -from .trace import trace + TYPE_CHECKING = False if TYPE_CHECKING: