Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions Lib/_pyrepl/_module_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,23 @@ def _find_modules(self, path: str, prefix: str) -> list[str]:
if path is None:
return []

modules: Iterable[pkgutil.ModuleInfo] = self.global_cache
modules: Iterable[pkgutil.ModuleInfo]
imported_module = sys.modules.get(path.split('.')[0])
if imported_module:
# Module already imported: only look in its location,
# even if a module with the same name would be higher in path
imported_path = (imported_module.__spec__
and imported_module.__spec__.origin)
if not imported_path:
# Module imported but no spec/origin: propose no suggestions
return []
if os.path.basename(imported_path) == "__init__.py": # package
imported_path = os.path.dirname(imported_path)
import_location = os.path.dirname(imported_path)
modules = list(pkgutil.iter_modules([import_location]))
else:
modules = self.global_cache

is_stdlib_import: bool | None = None
for segment in path.split('.'):
modules = [mod_info for mod_info in modules
Expand Down Expand Up @@ -196,7 +212,6 @@ def global_cache(self) -> list[pkgutil.ModuleInfo]:
"""Global module cache"""
if not self._global_cache or self._curr_sys_path != sys.path:
self._curr_sys_path = sys.path[:]
# print('getting packages')
self._global_cache = list(pkgutil.iter_modules())
return self._global_cache

Expand Down
72 changes: 70 additions & 2 deletions Lib/test/test_pyrepl/test_pyrepl.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import itertools
import os
import pathlib
import pkgutil
import re
import rlcompleter
import select
Expand Down Expand Up @@ -1090,17 +1091,84 @@ def test_hardcoded_stdlib_submodules(self):
self.assertEqual(output, expected)

def test_hardcoded_stdlib_submodules_not_proposed_if_local_import(self):
with tempfile.TemporaryDirectory() as _dir:
with (tempfile.TemporaryDirectory() as _dir,
patch.object(sys, "modules", {})): # hide imported module
dir = pathlib.Path(_dir)
(dir / "collections").mkdir()
(dir / "collections" / "__init__.py").touch()
(dir / "collections" / "foo.py").touch()
with patch.object(sys, "path", [dir, *sys.path]):
with patch.object(sys, "path", [_dir, *sys.path]):
events = code_to_events("import collections.\t\n")
reader = self.prepare_reader(events, namespace={})
output = reader.readline()
self.assertEqual(output, "import collections.foo")

def test_already_imported_stdlib_module_no_other_suggestions(self):
with (tempfile.TemporaryDirectory() as _dir,
patch.object(sys, "path", [_dir, *sys.path])):
dir = pathlib.Path(_dir)
(dir / "collections").mkdir()
(dir / "collections" / "__init__.py").touch()
(dir / "collections" / "foo.py").touch()

# collections found in dir, but was already imported
# from stdlib at startup -> suggest stdlib submodules only
events = code_to_events("import collections.\t\n")
reader = self.prepare_reader(events, namespace={})
output = reader.readline()
self.assertEqual(output, "import collections.abc")

def test_already_imported_custom_module_no_other_suggestions(self):
with (tempfile.TemporaryDirectory() as _dir1,
tempfile.TemporaryDirectory() as _dir2,
patch.object(sys, "path", [_dir2, _dir1, *sys.path])):
dir1 = pathlib.Path(_dir1)
(dir1 / "mymodule").mkdir()
(dir1 / "mymodule" / "__init__.py").touch()
(dir1 / "mymodule" / "foo.py").touch()
importlib.import_module("mymodule")

dir2 = pathlib.Path(_dir2)
(dir2 / "mymodule").mkdir()
(dir2 / "mymodule" / "__init__.py").touch()
(dir2 / "mymodule" / "bar.py").touch()
# Purge FileFinder cache after adding files
pkgutil.get_importer(_dir2).invalidate_caches()
# mymodule found in dir2 before dir1, but it was already imported
# from dir1 -> suggest dir1 submodules only
events = code_to_events("import mymodule.\t\n")
reader = self.prepare_reader(events, namespace={})
output = reader.readline()
self.assertEqual(output, "import mymodule.foo")

del sys.modules["mymodule"]
# mymodule not imported anymore -> suggest dir2 submodules
events = code_to_events("import mymodule.\t\n")
reader = self.prepare_reader(events, namespace={})
output = reader.readline()
self.assertEqual(output, "import mymodule.bar")

def test_already_imported_custom_file_no_suggestions(self):
# Same as before, but mymodule from dir1 has no submodules
# -> propose nothing
with (tempfile.TemporaryDirectory() as _dir1,
tempfile.TemporaryDirectory() as _dir2,
patch.object(sys, "path", [_dir2, _dir1, *sys.path])):
dir1 = pathlib.Path(_dir1)
(dir1 / "mymodule").mkdir()
(dir1 / "mymodule.py").touch()
importlib.import_module("mymodule")

dir2 = pathlib.Path(_dir2)
(dir2 / "mymodule").mkdir()
(dir2 / "mymodule" / "__init__.py").touch()
(dir2 / "mymodule" / "bar.py").touch()
events = code_to_events("import mymodule.\t\n")
reader = self.prepare_reader(events, namespace={})
output = reader.readline()
self.assertEqual(output, "import mymodule.")
del sys.modules["mymodule"]

def test_get_path_and_prefix(self):
cases = (
('', ('', '')),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix edge-cases around already imported modules in the :term:`REPL`
auto-completion of imports.
Loading