Skip to content

Commit d332e14

Browse files
committed
Check __spec__.has_location + refactor
1 parent 5fa70cf commit d332e14

File tree

2 files changed

+40
-12
lines changed

2 files changed

+40
-12
lines changed

Lib/_pyrepl/_module_completer.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
TYPE_CHECKING = False
1717

1818
if TYPE_CHECKING:
19+
from types import ModuleType
1920
from typing import Any, Iterable, Iterator, Mapping
2021

2122

@@ -110,17 +111,7 @@ def _find_modules(self, path: str, prefix: str) -> list[str]:
110111
modules: Iterable[pkgutil.ModuleInfo]
111112
imported_module = sys.modules.get(path.split('.')[0])
112113
if imported_module:
113-
# Module already imported: only look in its location,
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 not imported_path:
118-
# Module imported but no spec/origin: propose no suggestions
119-
return []
120-
if os.path.basename(imported_path) == "__init__.py": # package
121-
imported_path = os.path.dirname(imported_path)
122-
import_location = os.path.dirname(imported_path)
123-
modules = list(pkgutil.iter_modules([import_location]))
114+
modules = self._find_already_imported_module_specs(imported_module)
124115
else:
125116
modules = self.global_cache
126117

@@ -145,6 +136,32 @@ def _is_stdlib_module(self, module_info: pkgutil.ModuleInfo) -> bool:
145136
return (isinstance(module_info.module_finder, FileFinder)
146137
and module_info.module_finder.path == self._stdlib_path)
147138

139+
def _find_already_imported_module_specs(self, imported_module: ModuleType) -> list[pkgutil.ModuleInfo]:
140+
# Module already imported: only look in its location,
141+
# even if a module with the same name would be higher in path
142+
module_location = self._get_module_location(imported_module)
143+
if not module_location:
144+
# If we cannot find the module source, propose no suggestions
145+
return []
146+
import_location = os.path.dirname(module_location)
147+
return list(pkgutil.iter_modules([import_location]))
148+
149+
def _get_module_location(self, imported_module: ModuleType) -> str | None:
150+
spec = imported_module.__spec__
151+
if not spec:
152+
return None
153+
if not spec.has_location:
154+
if spec.origin == "frozen": # See Tools/build/freeze_modules.py
155+
return os.path.join(self._stdlib_path, f"{spec.name}.py")
156+
return None
157+
if not spec.origin:
158+
return None
159+
if imported_module.__package__:
160+
# Package: the module location is the parent folder
161+
return os.path.dirname(spec.origin)
162+
else:
163+
return spec.origin
164+
148165
def is_suggestion_match(self, module_name: str, prefix: str) -> bool:
149166
if prefix:
150167
return module_name.startswith(prefix)

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,7 @@ def test_import_completions(self):
972972
("from importlib import mac\t\n", "from importlib import machinery"),
973973
("from importlib import res\t\n", "from importlib import resources"),
974974
("from importlib.res\t import a\t\n", "from importlib.resources import abc"),
975+
("from __phello__ import s\t\n", "from __phello__ import spam"), # frozen module
975976
)
976977
for code, expected in cases:
977978
with self.subTest(code=code):
@@ -1132,6 +1133,14 @@ def test_already_imported_stdlib_module_no_other_suggestions(self):
11321133
output = reader.readline()
11331134
self.assertEqual(output, "import collections.abc")
11341135

1136+
@patch.dict(sys.modules)
1137+
def test_already_imported_frozen_module(self):
1138+
importlib.import_module("__phello__")
1139+
events = code_to_events("from __phello__ import s\t\n")
1140+
reader = self.prepare_reader(events, namespace={})
1141+
output = reader.readline()
1142+
self.assertEqual(output, "from __phello__ import spam")
1143+
11351144
def test_already_imported_custom_module_no_other_suggestions(self):
11361145
with (tempfile.TemporaryDirectory() as _dir1,
11371146
tempfile.TemporaryDirectory() as _dir2,
@@ -1187,14 +1196,16 @@ def test_already_imported_module_without_origin_or_spec(self):
11871196
with (tempfile.TemporaryDirectory() as _dir1,
11881197
patch.object(sys, "path", [_dir1, *sys.path])):
11891198
dir1 = pathlib.Path(_dir1)
1190-
for mod in ("no_origin", "no_spec"):
1199+
for mod in ("no_origin", "not_has_location", "no_spec"):
11911200
(dir1 / mod).mkdir()
11921201
(dir1 / mod / "__init__.py").touch()
11931202
(dir1 / mod / "foo.py").touch()
11941203
module = importlib.import_module(mod)
11951204
assert module.__spec__
11961205
if mod == "no_origin":
11971206
module.__spec__.origin = None
1207+
elif mod == "not_has_location":
1208+
module.__spec__.has_location = False
11981209
else:
11991210
module.__spec__ = None
12001211
events = code_to_events(f"import {mod}.\t\n")

0 commit comments

Comments
 (0)