From 6ed27763e9d4a98b92f6ccf8f22ebd71762e26e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Tue, 30 Sep 2025 21:38:18 +0200 Subject: [PATCH 01/14] PyREPL module completion: check for already imported modules --- Lib/_pyrepl/_module_completer.py | 20 ++++++++- Lib/test/test_pyrepl/test_pyrepl.py | 69 ++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index cf59e007f4df80..eee108709d40e9 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -107,7 +107,25 @@ 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 for its submodules, + # even if a module with the same name would be higher in path + imported_path = (imported_module.__spec__ + and imported_module.__spec__.origin) + if imported_path: + 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: + # Module already imported but without spec/origin: + # propose no suggestions + modules = [] + else: + modules = self.global_cache + is_stdlib_import: bool | None = None for segment in path.split('.'): modules = [mod_info for mod_info in modules diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 47d384a209e9ac..c58c1efcbf723b 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1090,17 +1090,82 @@ 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() + # 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 = ( ('', ('', '')), From 48fd43f8e81c03b8840b9637a55641e40846d808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Tue, 30 Sep 2025 22:00:09 +0200 Subject: [PATCH 02/14] Add blurb --- .../2025-09-30-21-59-56.gh-issue-69605.qcmGF3.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-09-30-21-59-56.gh-issue-69605.qcmGF3.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-30-21-59-56.gh-issue-69605.qcmGF3.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-30-21-59-56.gh-issue-69605.qcmGF3.rst new file mode 100644 index 00000000000000..56d74d2583939b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-30-21-59-56.gh-issue-69605.qcmGF3.rst @@ -0,0 +1,2 @@ +Fix edge-cases around already imported modules in the :term:`REPL` +auto-completion of imports. From 7ac428e9c4029b02577b8a2e72ae2101af3c1c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Thu, 2 Oct 2025 01:48:15 +0200 Subject: [PATCH 03/14] Better convey intent --- Lib/_pyrepl/_module_completer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index eee108709d40e9..dac480fb78df00 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -110,7 +110,7 @@ def _find_modules(self, path: str, prefix: str) -> list[str]: modules: Iterable[pkgutil.ModuleInfo] imported_module = sys.modules.get(path.split('.')[0]) if imported_module: - # Module already imported: only look for its submodules, + # 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) @@ -122,7 +122,7 @@ def _find_modules(self, path: str, prefix: str) -> list[str]: else: # Module already imported but without spec/origin: # propose no suggestions - modules = [] + return [] else: modules = self.global_cache From 6515e2fefd7d8b81eaecab72cb81c029fd7cf534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Thu, 2 Oct 2025 01:49:28 +0200 Subject: [PATCH 04/14] [TEMP] debug tests on windows using modern technology (print statements) --- Lib/_pyrepl/_module_completer.py | 3 +++ Lib/test/test_pyrepl/test_pyrepl.py | 1 + 2 files changed, 4 insertions(+) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index dac480fb78df00..e177ecaa7e90db 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -130,12 +130,15 @@ def _find_modules(self, path: str, prefix: str) -> list[str]: for segment in path.split('.'): modules = [mod_info for mod_info in modules if mod_info.ispkg and mod_info.name == segment] + print(f"{segment=}, {modules=}") # TEMPORARY -- debugging tests on windows if is_stdlib_import is None: # Top-level import decide if we import from stdlib or not is_stdlib_import = all( self._is_stdlib_module(mod_info) for mod_info in modules ) modules = self.iter_submodules(modules) + modules = list(modules) # TEMPORARY -- debugging tests on windows + print(f"segment=last, {modules=}") # TEMPORARY -- debugging tests on windows module_names = [module.name for module in modules] if is_stdlib_import: diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index c58c1efcbf723b..591945d3d04bd0 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1139,6 +1139,7 @@ def test_already_imported_custom_module_no_other_suggestions(self): self.assertEqual(output, "import mymodule.foo") del sys.modules["mymodule"] + print(f"{dir1=}, {dir2=}") # TEMPORARY -- debugging tests on windows # mymodule not imported anymore -> suggest dir2 submodules events = code_to_events("import mymodule.\t\n") reader = self.prepare_reader(events, namespace={}) From 7dbb906e6e86d6bcfa6215698c76ae20d3222f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Thu, 2 Oct 2025 23:07:29 +0200 Subject: [PATCH 05/14] [TEMP] More debugging, where is my module?? --- Lib/test/test_pyrepl/test_pyrepl.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 591945d3d04bd0..49aacab811f1ea 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1140,6 +1140,8 @@ def test_already_imported_custom_module_no_other_suggestions(self): del sys.modules["mymodule"] print(f"{dir1=}, {dir2=}") # TEMPORARY -- debugging tests on windows + print(f"{[p.relative_to(dir1) for p in dir1.glob("**")]=}") # TEMPORARY -- debugging tests on windows + print(f"{[p.relative_to(dir2) for p in dir2.glob("**")]=}") # TEMPORARY -- debugging tests on windows # mymodule not imported anymore -> suggest dir2 submodules events = code_to_events("import mymodule.\t\n") reader = self.prepare_reader(events, namespace={}) From ac3065abbc9d19628a74729d9d8db39ae645422c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Thu, 2 Oct 2025 23:40:34 +0200 Subject: [PATCH 06/14] [TEMP] More debugging, where is my module?? (bis) --- Lib/_pyrepl/_module_completer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index e177ecaa7e90db..3cea986e782fd3 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -217,8 +217,10 @@ 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') + print('getting packages') # TEMPORARY -- debugging tests on windows self._global_cache = list(pkgutil.iter_modules()) + mymods = [p for p in self._global_cache if p.name == "mymodule"] # TEMPORARY -- debugging tests on windows + print("modules:", mymods, list(self.iter_submodules(mymods))) # TEMPORARY -- debugging tests on windows return self._global_cache From 75a33da261026388602cb64cddd8e5a109a6706d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Fri, 3 Oct 2025 00:33:00 +0200 Subject: [PATCH 07/14] [TEMP] Day 57, deep into debugging, I still don't know where is my module --- Lib/_pyrepl/_module_completer.py | 59 ++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index 3cea986e782fd3..ae246530bd5406 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -219,8 +219,63 @@ def global_cache(self) -> list[pkgutil.ModuleInfo]: self._curr_sys_path = sys.path[:] print('getting packages') # TEMPORARY -- debugging tests on windows self._global_cache = list(pkgutil.iter_modules()) - mymods = [p for p in self._global_cache if p.name == "mymodule"] # TEMPORARY -- debugging tests on windows - print("modules:", mymods, list(self.iter_submodules(mymods))) # TEMPORARY -- debugging tests on windows + # === BEGIN TEMPORARY -- debugging tests on windows === + mymod = next((p for p in self._global_cache if p.name == "mymodule"), None) + if mymod: + spec = mymod.module_finder.find_spec(mymod.name, None) + if spec: + print("1") + assert spec.submodule_search_locations and len(spec.submodule_search_locations) == 1 + print("2") + importer = pkgutil.get_importer(spec.submodule_search_locations[0]) + print("3") + assert importer and isinstance(importer, FileFinder) + print("4") + if importer.path is None or not os.path.isdir(importer.path): + print("4a") + return + yielded = {} + import inspect + try: + filenames = os.listdir(importer.path) + except OSError: + # ignore unreadable directories like import does + print("4b") + filenames = [] + print("4c", filenames) + filenames.sort() # handle packages before same-named modules + submods = [] + for fn in filenames: + print("4d", fn) + modname = inspect.getmodulename(fn) + print("4e", modname) + if modname=='__init__' or modname in yielded: + print("4f", modname) + continue + path = os.path.join(importer.path, fn) + ispkg = False + if not modname and os.path.isdir(path) and '.' not in fn: + print("4g") + modname = fn + try: + dircontents = os.listdir(path) + except OSError: + # ignore unreadable directories like import does + dircontents = [] + for fn in dircontents: + subname = inspect.getmodulename(fn) + if subname=='__init__': + ispkg = True + break + else: + continue # not a package + if modname and '.' not in modname: + print("4h") + yielded[modname] = 1 + submods.append((importer, modname, ispkg)) + print("4i") + print("module:", mymod, submods) + # === END TEMPORARY === return self._global_cache From ce124b1f0052435eafb534db97b04a2b25d6c015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Fri, 3 Oct 2025 01:04:35 +0200 Subject: [PATCH 08/14] [TEMP] Moar logs --- Lib/_pyrepl/_module_completer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index ae246530bd5406..d8ce9610ced59c 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -217,11 +217,13 @@ 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') # TEMPORARY -- debugging tests on windows + print('getting packages/') # TEMPORARY -- debugging tests on windows self._global_cache = list(pkgutil.iter_modules()) # === BEGIN TEMPORARY -- debugging tests on windows === + print(f"\n\n{self._global_cache=}\n\n") mymod = next((p for p in self._global_cache if p.name == "mymodule"), None) if mymod: + print("0a", mymod) spec = mymod.module_finder.find_spec(mymod.name, None) if spec: print("1") From 3f362cd93e35f9cf980ffc9d5612ab934065c53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Fri, 3 Oct 2025 14:51:03 +0200 Subject: [PATCH 09/14] [TEMP] Is it a FileFinder cache issue?? --- Lib/_pyrepl/_module_completer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index d8ce9610ced59c..fd1de7c9535044 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -220,11 +220,16 @@ def global_cache(self) -> list[pkgutil.ModuleInfo]: print('getting packages/') # TEMPORARY -- debugging tests on windows self._global_cache = list(pkgutil.iter_modules()) # === BEGIN TEMPORARY -- debugging tests on windows === - print(f"\n\n{self._global_cache=}\n\n") mymod = next((p for p in self._global_cache if p.name == "mymodule"), None) if mymod: print("0a", mymod) + import glob + print("files on finder path:", glob.glob("**", root_dir=mymod.module_finder.path, recursive=True)) spec = mymod.module_finder.find_spec(mymod.name, None) + print("found spec:", spec) + mymod.module_finder.invalidate_caches() + spec = mymod.module_finder.find_spec(mymod.name, None) + print("found spec after invalidate:", spec) if spec: print("1") assert spec.submodule_search_locations and len(spec.submodule_search_locations) == 1 From ed8ce73838009f465a4de0f18db5c5a9236ea97d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Fri, 3 Oct 2025 15:12:36 +0200 Subject: [PATCH 10/14] [TEMP] Looks like a cache issue indeed --- Lib/_pyrepl/_module_completer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index fd1de7c9535044..ca9c5d67a3ffa8 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -228,8 +228,8 @@ def global_cache(self) -> list[pkgutil.ModuleInfo]: spec = mymod.module_finder.find_spec(mymod.name, None) print("found spec:", spec) mymod.module_finder.invalidate_caches() - spec = mymod.module_finder.find_spec(mymod.name, None) - print("found spec after invalidate:", spec) + not_cached_spec = mymod.module_finder.find_spec(mymod.name, None) + print("found spec after invalidate:", not_cached_spec) if spec: print("1") assert spec.submodule_search_locations and len(spec.submodule_search_locations) == 1 From 19c49bb878b3cdc2eae8028f1ed8e829d48389d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Fri, 3 Oct 2025 15:41:54 +0200 Subject: [PATCH 11/14] Tests: clean FileFinder cache --- Lib/test/test_pyrepl/test_pyrepl.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 49aacab811f1ea..bb529939ce93b5 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -3,6 +3,7 @@ import itertools import os import pathlib +import pkgutil import re import rlcompleter import select @@ -1131,6 +1132,8 @@ def test_already_imported_custom_module_no_other_suggestions(self): (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") From 16e44afeb7b5d123b94e507a531e13829dfd9be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Fri, 3 Oct 2025 15:42:27 +0200 Subject: [PATCH 12/14] Remove all debugging junk --- Lib/_pyrepl/_module_completer.py | 68 ----------------------------- Lib/test/test_pyrepl/test_pyrepl.py | 3 -- 2 files changed, 71 deletions(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index ca9c5d67a3ffa8..a1c67a69853f1b 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -130,15 +130,12 @@ def _find_modules(self, path: str, prefix: str) -> list[str]: for segment in path.split('.'): modules = [mod_info for mod_info in modules if mod_info.ispkg and mod_info.name == segment] - print(f"{segment=}, {modules=}") # TEMPORARY -- debugging tests on windows if is_stdlib_import is None: # Top-level import decide if we import from stdlib or not is_stdlib_import = all( self._is_stdlib_module(mod_info) for mod_info in modules ) modules = self.iter_submodules(modules) - modules = list(modules) # TEMPORARY -- debugging tests on windows - print(f"segment=last, {modules=}") # TEMPORARY -- debugging tests on windows module_names = [module.name for module in modules] if is_stdlib_import: @@ -217,72 +214,7 @@ 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/') # TEMPORARY -- debugging tests on windows self._global_cache = list(pkgutil.iter_modules()) - # === BEGIN TEMPORARY -- debugging tests on windows === - mymod = next((p for p in self._global_cache if p.name == "mymodule"), None) - if mymod: - print("0a", mymod) - import glob - print("files on finder path:", glob.glob("**", root_dir=mymod.module_finder.path, recursive=True)) - spec = mymod.module_finder.find_spec(mymod.name, None) - print("found spec:", spec) - mymod.module_finder.invalidate_caches() - not_cached_spec = mymod.module_finder.find_spec(mymod.name, None) - print("found spec after invalidate:", not_cached_spec) - if spec: - print("1") - assert spec.submodule_search_locations and len(spec.submodule_search_locations) == 1 - print("2") - importer = pkgutil.get_importer(spec.submodule_search_locations[0]) - print("3") - assert importer and isinstance(importer, FileFinder) - print("4") - if importer.path is None or not os.path.isdir(importer.path): - print("4a") - return - yielded = {} - import inspect - try: - filenames = os.listdir(importer.path) - except OSError: - # ignore unreadable directories like import does - print("4b") - filenames = [] - print("4c", filenames) - filenames.sort() # handle packages before same-named modules - submods = [] - for fn in filenames: - print("4d", fn) - modname = inspect.getmodulename(fn) - print("4e", modname) - if modname=='__init__' or modname in yielded: - print("4f", modname) - continue - path = os.path.join(importer.path, fn) - ispkg = False - if not modname and os.path.isdir(path) and '.' not in fn: - print("4g") - modname = fn - try: - dircontents = os.listdir(path) - except OSError: - # ignore unreadable directories like import does - dircontents = [] - for fn in dircontents: - subname = inspect.getmodulename(fn) - if subname=='__init__': - ispkg = True - break - else: - continue # not a package - if modname and '.' not in modname: - print("4h") - yielded[modname] = 1 - submods.append((importer, modname, ispkg)) - print("4i") - print("module:", mymod, submods) - # === END TEMPORARY === return self._global_cache diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index bb529939ce93b5..397c0e699a4774 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1142,9 +1142,6 @@ def test_already_imported_custom_module_no_other_suggestions(self): self.assertEqual(output, "import mymodule.foo") del sys.modules["mymodule"] - print(f"{dir1=}, {dir2=}") # TEMPORARY -- debugging tests on windows - print(f"{[p.relative_to(dir1) for p in dir1.glob("**")]=}") # TEMPORARY -- debugging tests on windows - print(f"{[p.relative_to(dir2) for p in dir2.glob("**")]=}") # TEMPORARY -- debugging tests on windows # mymodule not imported anymore -> suggest dir2 submodules events = code_to_events("import mymodule.\t\n") reader = self.prepare_reader(events, namespace={}) From 14f6175ad13260c4a53745dcca34e2bb44cd5210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Sun, 5 Oct 2025 15:42:15 +0200 Subject: [PATCH 13/14] Small if refactor --- Lib/_pyrepl/_module_completer.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index d8ce9610ced59c..9a4f0dfa4bb0ff 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -114,15 +114,13 @@ def _find_modules(self, path: str, prefix: str) -> list[str]: # even if a module with the same name would be higher in path imported_path = (imported_module.__spec__ and imported_module.__spec__.origin) - if imported_path: - 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: - # Module already imported but without spec/origin: - # propose no suggestions + 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 From 78e47372a91c6868bd459f03faf36604af0967d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Simon?= Date: Sun, 12 Oct 2025 00:07:57 +0200 Subject: [PATCH 14/14] Full test coverage for new code --- Lib/test/test_pyrepl/test_pyrepl.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 397c0e699a4774..927fc0a71ac246 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1169,6 +1169,26 @@ def test_already_imported_custom_file_no_suggestions(self): self.assertEqual(output, "import mymodule.") del sys.modules["mymodule"] + def test_already_imported_module_without_origin_or_spec(self): + with (tempfile.TemporaryDirectory() as _dir1, + patch.object(sys, "path", [_dir1, *sys.path])): + dir1 = pathlib.Path(_dir1) + for mod in ("no_origin", "no_spec"): + (dir1 / mod).mkdir() + (dir1 / mod / "__init__.py").touch() + (dir1 / mod / "foo.py").touch() + module = importlib.import_module(mod) + assert module.__spec__ + if mod == "no_origin": + module.__spec__.origin = None + else: + module.__spec__ = None + events = code_to_events(f"import {mod}.\t\n") + reader = self.prepare_reader(events, namespace={}) + output = reader.readline() + self.assertEqual(output, f"import {mod}.") + del sys.modules[mod] + def test_get_path_and_prefix(self): cases = ( ('', ('', '')),