gh-134215: PyREPL: Do not show underscored modules by default during autocompletion (gh-134267)

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>
This commit is contained in:
Kevin Hernández 2025-05-20 16:26:48 -04:00 committed by GitHub
parent c91ad5da9d
commit a3a3cf6d15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 57 additions and 5 deletions

View file

@ -81,8 +81,10 @@ class ModuleCompleter:
def _find_modules(self, path: str, prefix: str) -> list[str]: def _find_modules(self, path: str, prefix: str) -> list[str]:
if not path: if not path:
# Top-level import (e.g. `import foo<tab>`` or `from foo<tab>`)` # 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)] builtin_modules = [name for name in sys.builtin_module_names
third_party_modules = [name for _, name, _ in self.global_cache if name.startswith(prefix)] 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) return sorted(builtin_modules + third_party_modules)
if path.startswith('.'): if path.startswith('.'):
@ -98,7 +100,14 @@ class ModuleCompleter:
if mod_info.ispkg and mod_info.name == segment] if mod_info.ispkg and mod_info.name == segment]
modules = self.iter_submodules(modules) modules = self.iter_submodules(modules)
return [module.name for module in 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]: def iter_submodules(self, parent_modules: list[pkgutil.ModuleInfo]) -> Iterator[pkgutil.ModuleInfo]:
"""Iterate over all submodules of the given parent modules.""" """Iterate over all submodules of the given parent modules."""

View file

@ -8,6 +8,7 @@ import select
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
from pkgutil import ModuleInfo
from unittest import TestCase, skipUnless, skipIf from unittest import TestCase, skipUnless, skipIf
from unittest.mock import patch from unittest.mock import patch
from test.support import force_not_colorized, make_clean_env, Py_DEBUG from test.support import force_not_colorized, make_clean_env, Py_DEBUG
@ -959,6 +960,46 @@ class TestPyReplModuleCompleter(TestCase):
output = reader.readline() output = reader.readline()
self.assertEqual(output, expected) 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): def test_builtin_completion_top_level(self):
import importlib import importlib
# Make iter_modules() search only the standard library. # Make iter_modules() search only the standard library.
@ -991,8 +1032,8 @@ class TestPyReplModuleCompleter(TestCase):
output = reader.readline() output = reader.readline()
self.assertEqual(output, expected) self.assertEqual(output, expected)
@patch("pkgutil.iter_modules", lambda: [(None, 'valid_name', None), @patch("pkgutil.iter_modules", lambda: [ModuleInfo(None, "valid_name", True),
(None, 'invalid-name', None)]) ModuleInfo(None, "invalid-name", True)])
def test_invalid_identifiers(self): def test_invalid_identifiers(self):
# Make sure modules which are not valid identifiers # Make sure modules which are not valid identifiers
# are not suggested as those cannot be imported via 'import'. # are not suggested as those cannot be imported via 'import'.

View file

@ -763,6 +763,7 @@ Chris Herborth
Ivan Herman Ivan Herman
Jürgen Hermann Jürgen Hermann
Joshua Jay Herman Joshua Jay Herman
Kevin Hernandez
Gary Herron Gary Herron
Ernie Hershey Ernie Hershey
Thomas Herve Thomas Herve

View file

@ -0,0 +1 @@
:term:`REPL` import autocomplete only suggests private modules when explicitly specified.