Skip to content

Commit 6ed2776

Browse files
committed
PyREPL module completion: check for already imported modules
1 parent 98a41af commit 6ed2776

File tree

2 files changed

+86
-3
lines changed

2 files changed

+86
-3
lines changed

Lib/_pyrepl/_module_completer.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,25 @@ def _find_modules(self, path: str, prefix: str) -> list[str]:
107107
if path is None:
108108
return []
109109

110-
modules: Iterable[pkgutil.ModuleInfo] = self.global_cache
110+
modules: Iterable[pkgutil.ModuleInfo]
111+
imported_module = sys.modules.get(path.split('.')[0])
112+
if imported_module:
113+
# Module already imported: only look for its submodules,
114+
# even if a module with the same name would be higher in path
115+
imported_path = (imported_module.__spec__
116+
and imported_module.__spec__.origin)
117+
if imported_path:
118+
if os.path.basename(imported_path) == "__init__.py": # package
119+
imported_path = os.path.dirname(imported_path)
120+
import_location = os.path.dirname(imported_path)
121+
modules = list(pkgutil.iter_modules([import_location]))
122+
else:
123+
# Module already imported but without spec/origin:
124+
# propose no suggestions
125+
modules = []
126+
else:
127+
modules = self.global_cache
128+
111129
is_stdlib_import: bool | None = None
112130
for segment in path.split('.'):
113131
modules = [mod_info for mod_info in modules

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,17 +1090,82 @@ def test_hardcoded_stdlib_submodules(self):
10901090
self.assertEqual(output, expected)
10911091

10921092
def test_hardcoded_stdlib_submodules_not_proposed_if_local_import(self):
1093-
with tempfile.TemporaryDirectory() as _dir:
1093+
with (tempfile.TemporaryDirectory() as _dir,
1094+
patch.object(sys, "modules", {})): # hide imported module
10941095
dir = pathlib.Path(_dir)
10951096
(dir / "collections").mkdir()
10961097
(dir / "collections" / "__init__.py").touch()
10971098
(dir / "collections" / "foo.py").touch()
1098-
with patch.object(sys, "path", [dir, *sys.path]):
1099+
with patch.object(sys, "path", [_dir, *sys.path]):
10991100
events = code_to_events("import collections.\t\n")
11001101
reader = self.prepare_reader(events, namespace={})
11011102
output = reader.readline()
11021103
self.assertEqual(output, "import collections.foo")
11031104

1105+
def test_already_imported_stdlib_module_no_other_suggestions(self):
1106+
with (tempfile.TemporaryDirectory() as _dir,
1107+
patch.object(sys, "path", [_dir, *sys.path])):
1108+
dir = pathlib.Path(_dir)
1109+
(dir / "collections").mkdir()
1110+
(dir / "collections" / "__init__.py").touch()
1111+
(dir / "collections" / "foo.py").touch()
1112+
1113+
# collections found in dir, but was already imported
1114+
# from stdlib at startup -> suggest stdlib submodules only
1115+
events = code_to_events("import collections.\t\n")
1116+
reader = self.prepare_reader(events, namespace={})
1117+
output = reader.readline()
1118+
self.assertEqual(output, "import collections.abc")
1119+
1120+
def test_already_imported_custom_module_no_other_suggestions(self):
1121+
with (tempfile.TemporaryDirectory() as _dir1,
1122+
tempfile.TemporaryDirectory() as _dir2,
1123+
patch.object(sys, "path", [_dir2, _dir1, *sys.path])):
1124+
dir1 = pathlib.Path(_dir1)
1125+
(dir1 / "mymodule").mkdir()
1126+
(dir1 / "mymodule" / "__init__.py").touch()
1127+
(dir1 / "mymodule" / "foo.py").touch()
1128+
importlib.import_module("mymodule")
1129+
1130+
dir2 = pathlib.Path(_dir2)
1131+
(dir2 / "mymodule").mkdir()
1132+
(dir2 / "mymodule" / "__init__.py").touch()
1133+
(dir2 / "mymodule" / "bar.py").touch()
1134+
# mymodule found in dir2 before dir1, but it was already imported
1135+
# from dir1 -> suggest dir1 submodules only
1136+
events = code_to_events("import mymodule.\t\n")
1137+
reader = self.prepare_reader(events, namespace={})
1138+
output = reader.readline()
1139+
self.assertEqual(output, "import mymodule.foo")
1140+
1141+
del sys.modules["mymodule"]
1142+
# mymodule not imported anymore -> suggest dir2 submodules
1143+
events = code_to_events("import mymodule.\t\n")
1144+
reader = self.prepare_reader(events, namespace={})
1145+
output = reader.readline()
1146+
self.assertEqual(output, "import mymodule.bar")
1147+
1148+
def test_already_imported_custom_file_no_suggestions(self):
1149+
# Same as before, but mymodule from dir1 has no submodules
1150+
# -> propose nothing
1151+
with (tempfile.TemporaryDirectory() as _dir1,
1152+
tempfile.TemporaryDirectory() as _dir2,
1153+
patch.object(sys, "path", [_dir2, _dir1, *sys.path])):
1154+
dir1 = pathlib.Path(_dir1)
1155+
(dir1 / "mymodule").mkdir()
1156+
(dir1 / "mymodule.py").touch()
1157+
importlib.import_module("mymodule")
1158+
1159+
dir2 = pathlib.Path(_dir2)
1160+
(dir2 / "mymodule").mkdir()
1161+
(dir2 / "mymodule" / "__init__.py").touch()
1162+
(dir2 / "mymodule" / "bar.py").touch()
1163+
events = code_to_events("import mymodule.\t\n")
1164+
reader = self.prepare_reader(events, namespace={})
1165+
output = reader.readline()
1166+
self.assertEqual(output, "import mymodule.")
1167+
del sys.modules["mymodule"]
1168+
11041169
def test_get_path_and_prefix(self):
11051170
cases = (
11061171
('', ('', '')),

0 commit comments

Comments
 (0)