]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-134215: PyREPL: Do not show underscored modules by default during autocompletion...
authorKevin Hernández <kevin.hernandez@unet.edu.ve>
Tue, 20 May 2025 20:26:48 +0000 (16:26 -0400)
committerGitHub <noreply@github.com>
Tue, 20 May 2025 20:26:48 +0000 (22:26 +0200)
Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
Co-authored-by: Tomas R. <tomas.roun8@gmail.com>
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
Lib/_pyrepl/_module_completer.py
Lib/test/test_pyrepl/test_pyrepl.py
Misc/ACKS
Misc/NEWS.d/next/Tools-Demos/2025-05-19-14-57-46.gh-issue-134215.sbdDK6.rst [new file with mode: 0644]

index 0606797226d1e04268fe0fa0c743301c3f57d2f0..9aafb55090e2ce1c3e49078b6da296c8b904bbbf 100644 (file)
@@ -81,8 +81,10 @@ class ModuleCompleter:
     def _find_modules(self, path: str, prefix: str) -> list[str]:
         if not path:
             # Top-level import (e.g. `import foo<tab>`` or `from foo<tab>`)`
-            builtin_modules = [name for name in sys.builtin_module_names if name.startswith(prefix)]
-            third_party_modules = [name for _, name, _ in self.global_cache if name.startswith(prefix)]
+            builtin_modules = [name for name in sys.builtin_module_names
+                               if self.is_suggestion_match(name, prefix)]
+            third_party_modules = [module.name for module in self.global_cache
+                                   if self.is_suggestion_match(module.name, prefix)]
             return sorted(builtin_modules + third_party_modules)
 
         if path.startswith('.'):
@@ -98,7 +100,14 @@ class ModuleCompleter:
                        if mod_info.ispkg and mod_info.name == segment]
             modules = self.iter_submodules(modules)
         return [module.name for module in modules
-                if module.name.startswith(prefix)]
+                if self.is_suggestion_match(module.name, prefix)]
+
+    def is_suggestion_match(self, module_name: str, prefix: str) -> bool:
+        if prefix:
+            return module_name.startswith(prefix)
+        # For consistency with attribute completion, which
+        # does not suggest private attributes unless requested.
+        return not module_name.startswith("_")
 
     def iter_submodules(self, parent_modules: list[pkgutil.ModuleInfo]) -> Iterator[pkgutil.ModuleInfo]:
         """Iterate over all submodules of the given parent modules."""
index 59f5d1f893f9fd81be45caf9ebdd58b35c879536..29762232d43b890a8943f74c4b7eb9d7d47de8ae 100644 (file)
@@ -8,6 +8,7 @@ import select
 import subprocess
 import sys
 import tempfile
+from pkgutil import ModuleInfo
 from unittest import TestCase, skipUnless, skipIf
 from unittest.mock import patch
 from test.support import force_not_colorized, make_clean_env, Py_DEBUG
@@ -959,6 +960,46 @@ class TestPyReplModuleCompleter(TestCase):
                 output = reader.readline()
                 self.assertEqual(output, expected)
 
+    @patch("pkgutil.iter_modules", lambda: [ModuleInfo(None, "public", True),
+                                            ModuleInfo(None, "_private", True)])
+    @patch("sys.builtin_module_names", ())
+    def test_private_completions(self):
+        cases = (
+            # Return public methods by default
+            ("import \t\n", "import public"),
+            ("from \t\n", "from public"),
+            # Return private methods if explicitly specified
+            ("import _\t\n", "import _private"),
+            ("from _\t\n", "from _private"),
+        )
+        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)
+
+    @patch(
+        "_pyrepl._module_completer.ModuleCompleter.iter_submodules",
+        lambda *_: [
+            ModuleInfo(None, "public", True),
+            ModuleInfo(None, "_private", True),
+        ],
+    )
+    def test_sub_module_private_completions(self):
+        cases = (
+            # Return public methods by default
+            ("from foo import \t\n", "from foo import public"),
+            # Return private methods if explicitly specified
+            ("from foo import _\t\n", "from foo import _private"),
+        )
+        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_builtin_completion_top_level(self):
         import importlib
         # Make iter_modules() search only the standard library.
@@ -991,8 +1032,8 @@ class TestPyReplModuleCompleter(TestCase):
                 output = reader.readline()
                 self.assertEqual(output, expected)
 
-    @patch("pkgutil.iter_modules", lambda: [(None, 'valid_name', None),
-                                            (None, 'invalid-name', None)])
+    @patch("pkgutil.iter_modules", lambda: [ModuleInfo(None, "valid_name", True),
+                                            ModuleInfo(None, "invalid-name", True)])
     def test_invalid_identifiers(self):
         # Make sure modules which are not valid identifiers
         # are not suggested as those cannot be imported via 'import'.
index 1b500870dec4726bf8b5bf165590715271674d49..571142e7e49763f897475d6bb2b2e3ce04736dc8 100644 (file)
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -763,6 +763,7 @@ Chris Herborth
 Ivan Herman
 Jürgen Hermann
 Joshua Jay Herman
+Kevin Hernandez
 Gary Herron
 Ernie Hershey
 Thomas Herve
diff --git a/Misc/NEWS.d/next/Tools-Demos/2025-05-19-14-57-46.gh-issue-134215.sbdDK6.rst b/Misc/NEWS.d/next/Tools-Demos/2025-05-19-14-57-46.gh-issue-134215.sbdDK6.rst
new file mode 100644 (file)
index 0000000..546ed2a
--- /dev/null
@@ -0,0 +1 @@
+:term:`REPL` import autocomplete only suggests private modules when explicitly specified.