From 8978e9595c0cd6d99839a8b1bc3eb4ddc56b4327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Sun, 18 May 2025 16:29:35 +0200 Subject: [PATCH 1/7] PyREPL module autocomplete: do not fallback When no module completions are available, do not fallback to completions from current namespace --- Lib/_pyrepl/_module_completer.py | 9 ++++++--- Lib/_pyrepl/readline.py | 4 ++-- Lib/test/test_pyrepl/test_pyrepl.py | 3 +++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index 347f05607c75c5..e186f499787b2e 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -42,11 +42,14 @@ def __init__(self, namespace: Mapping[str, Any] | None = None) -> None: self._global_cache: list[pkgutil.ModuleInfo] = [] self._curr_sys_path: list[str] = sys.path[:] - def get_completions(self, line: str) -> list[str]: - """Return the next possible import completions for 'line'.""" + def get_completions(self, line: str) -> list[str] | None: + """Return the next possible import completions for 'line'. + + If 'line' is not an import statement, return None. + """ result = ImportParser(line).parse() if not result: - return [] + return None try: return self.complete(*result) except Exception: diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 560a9db192169e..ff1bec4c0b1311 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -134,7 +134,7 @@ def get_stem(self) -> str: return "".join(b[p + 1 : self.pos]) def get_completions(self, stem: str) -> list[str]: - if module_completions := self.get_module_completions(): + if (module_completions := self.get_module_completions()) is not None: return module_completions if len(stem) == 0 and self.more_lines is not None: b = self.buffer @@ -165,7 +165,7 @@ def get_completions(self, stem: str) -> list[str]: result.sort() return result - def get_module_completions(self) -> list[str]: + def get_module_completions(self) -> list[str] | None: line = self.get_line() return self.config.module_completer.get_completions(line) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index fc8114891d12dd..ba21077e2d385f 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -944,13 +944,16 @@ def test_import_completions(self): ("import importlib.resources.\t\ta\t\n", "import importlib.resources.abc"), ("import foo, impo\t\n", "import foo, importlib"), ("import foo as bar, impo\t\n", "import foo as bar, importlib"), + ("import pri\t\n", "import pri"), # do not complete with "print(" ("from impo\t\n", "from importlib"), + ("from pri\t\n", "from pri"), ("from importlib.res\t\n", "from importlib.resources"), ("from importlib.\t\tres\t\n", "from importlib.resources"), ("from importlib.resources.ab\t\n", "from importlib.resources.abc"), ("from importlib import mac\t\n", "from importlib import machinery"), ("from importlib import res\t\n", "from importlib import resources"), ("from importlib.res\t import a\t\n", "from importlib.resources import abc"), + ("from typing import Na\t\n", "from typing import Na"), # do not complete with "NameError(" ) for code, expected in cases: with self.subTest(code=code): From ccfdf43a9c7f58644a89009ea22638b152f3092c Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 18 May 2025 14:33:24 +0000 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2025-05-18-14-33-23.gh-issue-69605.ZMO49F.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-14-33-23.gh-issue-69605.ZMO49F.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-14-33-23.gh-issue-69605.ZMO49F.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-14-33-23.gh-issue-69605.ZMO49F.rst new file mode 100644 index 00000000000000..3d340635efc8f3 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-14-33-23.gh-issue-69605.ZMO49F.rst @@ -0,0 +1 @@ +Stop import names completion fallback on regular names completion in the :term:`REPL`. From 377c761c65d8672a971804018d46fecfab88c5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Mon, 19 May 2025 15:45:54 +0200 Subject: [PATCH 3/7] Improve blurb wording --- .../2025-05-18-14-33-23.gh-issue-69605.ZMO49F.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-14-33-23.gh-issue-69605.ZMO49F.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-14-33-23.gh-issue-69605.ZMO49F.rst index 3d340635efc8f3..7b7275fee69b9b 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-14-33-23.gh-issue-69605.ZMO49F.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-18-14-33-23.gh-issue-69605.ZMO49F.rst @@ -1 +1,2 @@ -Stop import names completion fallback on regular names completion in the :term:`REPL`. +When auto-completing an import in the :term:`REPL`, finding no candidates +now issues no suggestion, rather than suggestions from the current namespace. From 99e1980802cc8519e4494055bde9e37a8318c9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Mon, 19 May 2025 15:46:20 +0200 Subject: [PATCH 4/7] Get rid of assignment expression --- Lib/_pyrepl/readline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index ff1bec4c0b1311..dce7e3af3a122b 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -134,7 +134,8 @@ def get_stem(self) -> str: return "".join(b[p + 1 : self.pos]) def get_completions(self, stem: str) -> list[str]: - if (module_completions := self.get_module_completions()) is not None: + module_completions = self.get_module_completions() + if module_completions is not None: return module_completions if len(stem) == 0 and self.more_lines is not None: b = self.buffer From dd5940e15bac24243c3f7ea423f6c622ea409770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Mon, 19 May 2025 18:33:16 +0200 Subject: [PATCH 5/7] Move tests to a dedicated method --- Lib/test/test_pyrepl/test_pyrepl.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index ba21077e2d385f..3409ca14b3432f 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -929,7 +929,7 @@ def prepare_reader(self, events, namespace): reader = ReadlineAlikeReader(console=console, config=config) return reader - def test_import_completions(self): + def _only_stdlib_imports(self): import importlib # Make iter_modules() search only the standard library. # This makes the test more reliable in case there are @@ -938,22 +938,21 @@ def test_import_completions(self): lib_path = os.path.dirname(importlib.__path__[0]) sys.path = [lib_path] + def test_import_completions(self): + self._only_stdlib_imports() cases = ( ("import path\t\n", "import pathlib"), ("import importlib.\t\tres\t\n", "import importlib.resources"), ("import importlib.resources.\t\ta\t\n", "import importlib.resources.abc"), ("import foo, impo\t\n", "import foo, importlib"), ("import foo as bar, impo\t\n", "import foo as bar, importlib"), - ("import pri\t\n", "import pri"), # do not complete with "print(" ("from impo\t\n", "from importlib"), - ("from pri\t\n", "from pri"), ("from importlib.res\t\n", "from importlib.resources"), ("from importlib.\t\tres\t\n", "from importlib.resources"), ("from importlib.resources.ab\t\n", "from importlib.resources.abc"), ("from importlib import mac\t\n", "from importlib import machinery"), ("from importlib import res\t\n", "from importlib import resources"), ("from importlib.res\t import a\t\n", "from importlib.resources import abc"), - ("from typing import Na\t\n", "from typing import Na"), # do not complete with "NameError(" ) for code, expected in cases: with self.subTest(code=code): @@ -991,6 +990,20 @@ def test_invalid_identifiers(self): output = reader.readline() self.assertEqual(output, expected) + def test_no_fallback_on_regular_completion(self): + self._only_stdlib_imports() + cases = ( + ("import pri\t\n", "import pri"), + ("from pri\t\n", "from pri"), + ("from typing import Na\t\n", "from typing import Na"), + ) + for code, expected in cases: + with self.subTest(code=code): + events = code_to_events(code) + reader = self.prepare_reader(events, namespace={}) + output = reader.readline() + self.assertEqual(output, expected) + def test_get_path_and_prefix(self): cases = ( ('', ('', '')), From 84c4f765bde1ff500f5218281b7b60cd4d707d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Tue, 20 May 2025 20:03:12 +0200 Subject: [PATCH 6/7] Tests: mock sys.path in setUp instead --- Lib/test/test_pyrepl/test_pyrepl.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 3409ca14b3432f..988818deb379d3 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -917,7 +917,14 @@ def test_func(self): class TestPyReplModuleCompleter(TestCase): def setUp(self): + import importlib + # Make iter_modules() search only the standard library. + # This makes the test more reliable in case there are + # other user packages/scripts on PYTHONPATH which can + # interfere with the completions. + lib_path = os.path.dirname(importlib.__path__[0]) self._saved_sys_path = sys.path + sys.path = [lib_path] def tearDown(self): sys.path = self._saved_sys_path @@ -929,17 +936,7 @@ def prepare_reader(self, events, namespace): reader = ReadlineAlikeReader(console=console, config=config) return reader - def _only_stdlib_imports(self): - import importlib - # Make iter_modules() search only the standard library. - # This makes the test more reliable in case there are - # other user packages/scripts on PYTHONPATH which can - # intefere with the completions. - lib_path = os.path.dirname(importlib.__path__[0]) - sys.path = [lib_path] - def test_import_completions(self): - self._only_stdlib_imports() cases = ( ("import path\t\n", "import pathlib"), ("import importlib.\t\tres\t\n", "import importlib.resources"), @@ -991,7 +988,6 @@ def test_invalid_identifiers(self): self.assertEqual(output, expected) def test_no_fallback_on_regular_completion(self): - self._only_stdlib_imports() cases = ( ("import pri\t\n", "import pri"), ("from pri\t\n", "from pri"), From 719875ebfee0d240a852b5ff80c34921d5f9f2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Thu, 22 May 2025 13:35:40 +0200 Subject: [PATCH 7/7] Remove extra docstring line --- Lib/_pyrepl/_module_completer.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index e186f499787b2e..d6bb008b68a311 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -43,10 +43,7 @@ def __init__(self, namespace: Mapping[str, Any] | None = None) -> None: self._curr_sys_path: list[str] = sys.path[:] def get_completions(self, line: str) -> list[str] | None: - """Return the next possible import completions for 'line'. - - If 'line' is not an import statement, return None. - """ + """Return the next possible import completions for 'line'.""" result = ImportParser(line).parse() if not result: return None