Skip to content

Commit dc13676

Browse files
committed
PyREPL module completion: hardcode special stdlib submodules
1 parent 31d3836 commit dc13676

File tree

2 files changed

+47
-12
lines changed

2 files changed

+47
-12
lines changed

Lib/_pyrepl/_module_completer.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@
1616
from typing import Any, Iterable, Iterator, Mapping
1717

1818

19+
HARDCODED_SUBMODULES = {
20+
# Standard library submodules that are not detected by pkgutil.iter_modules
21+
# but can be imported, so should be proposed in completion
22+
"collections": ["abc"],
23+
"os": ["path"],
24+
"xml.parsers.expat": ["errors", "model"],
25+
}
26+
27+
1928
def make_default_module_completer() -> ModuleCompleter:
2029
# Inside pyrepl, __package__ is set to None by default
2130
return ModuleCompleter(namespace={'__package__': None})
@@ -99,8 +108,14 @@ def _find_modules(self, path: str, prefix: str) -> list[str]:
99108
modules = [mod_info for mod_info in modules
100109
if mod_info.ispkg and mod_info.name == segment]
101110
modules = self.iter_submodules(modules)
102-
return [module.name for module in modules
103-
if self.is_suggestion_match(module.name, prefix)]
111+
112+
module_names = [module.name for module in modules]
113+
try:
114+
module_names += HARDCODED_SUBMODULES[path]
115+
except KeyError:
116+
pass
117+
return [module_name for module_name in module_names
118+
if self.is_suggestion_match(module_name, prefix)]
104119

105120
def is_suggestion_match(self, module_name: str, prefix: str) -> bool:
106121
if prefix:

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import importlib
12
import io
23
import itertools
34
import os
@@ -26,7 +27,8 @@
2627
code_to_events,
2728
)
2829
from _pyrepl.console import Event
29-
from _pyrepl._module_completer import ImportParser, ModuleCompleter
30+
from _pyrepl._module_completer import (ImportParser, ModuleCompleter,
31+
HARDCODED_SUBMODULES)
3032
from _pyrepl.readline import (ReadlineAlikeReader, ReadlineConfig,
3133
_ReadlineWrapper)
3234
from _pyrepl.readline import multiline_input as readline_multiline_input
@@ -930,7 +932,6 @@ def test_func(self):
930932

931933
class TestPyReplModuleCompleter(TestCase):
932934
def setUp(self):
933-
import importlib
934935
# Make iter_modules() search only the standard library.
935936
# This makes the test more reliable in case there are
936937
# other user packages/scripts on PYTHONPATH which can
@@ -1013,14 +1014,6 @@ def test_sub_module_private_completions(self):
10131014
self.assertEqual(output, expected)
10141015

10151016
def test_builtin_completion_top_level(self):
1016-
import importlib
1017-
# Make iter_modules() search only the standard library.
1018-
# This makes the test more reliable in case there are
1019-
# other user packages/scripts on PYTHONPATH which can
1020-
# intefere with the completions.
1021-
lib_path = os.path.dirname(importlib.__path__[0])
1022-
sys.path = [lib_path]
1023-
10241017
cases = (
10251018
("import bui\t\n", "import builtins"),
10261019
("from bui\t\n", "from builtins"),
@@ -1076,6 +1069,20 @@ def test_no_fallback_on_regular_completion(self):
10761069
output = reader.readline()
10771070
self.assertEqual(output, expected)
10781071

1072+
def test_hardcoded_stdlib_submodules(self):
1073+
cases = (
1074+
("import collections.\t\n", "import collections.abc"),
1075+
("from os import \t\n", "from os import path"),
1076+
("import xml.parsers.expat.\t\te\t\n\n", "import xml.parsers.expat.errors"),
1077+
("from xml.parsers.expat import \t\tm\t\n\n", "from xml.parsers.expat import model"),
1078+
)
1079+
for code, expected in cases:
1080+
with self.subTest(code=code):
1081+
events = code_to_events(code)
1082+
reader = self.prepare_reader(events, namespace={})
1083+
output = reader.readline()
1084+
self.assertEqual(output, expected)
1085+
10791086
def test_get_path_and_prefix(self):
10801087
cases = (
10811088
('', ('', '')),
@@ -1204,6 +1211,19 @@ def test_parse_error(self):
12041211
with self.subTest(code=code):
12051212
self.assertEqual(actual, None)
12061213

1214+
1215+
class TestHardcodedSubmodules(TestCase):
1216+
def test_hardcoded_stdlib_submodules_are_importable(self):
1217+
for parent_path, submodules in HARDCODED_SUBMODULES.items():
1218+
for module_name in submodules:
1219+
path = f"{parent_path}.{module_name}"
1220+
with self.subTest(path=path):
1221+
# We can't use importlib.util.find_spec here,
1222+
# since some hardcoded submodules parents are
1223+
# not proper packages
1224+
importlib.import_module(path)
1225+
1226+
12071227
class TestPasteEvent(TestCase):
12081228
def prepare_reader(self, events):
12091229
console = FakeConsole(events)

0 commit comments

Comments
 (0)