From b5eadf7812e4cb47b3602bfbd6dfa1f421f21f20 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 20 Aug 2026 22:30:17 -0700 Subject: [PATCH 1/8] gh-149167: PyREPL autocomplete imports to only display public members Adds a mechanism that filters a module's public members for showing on autocomplete. --- Lib/_pyrepl/_module_completer.py | 5 ++++- Lib/test/test_pyrepl/test_pyrepl.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index 17bf5cdc819542..f19986704528fa 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -221,7 +221,10 @@ def _find_attributes( if not imported_module: return [], None, self._get_import_completion_action(path) try: - module_attributes = dir(imported_module) + if hasattr(imported_module, '__all__'): # Use __all__ if available, otherwise use dir() + module_attributes = imported_module.__all__ + else: + module_attributes = dir(imported_module) except Exception: module_attributes = [] # Filter out invalid attribute names, such as dashes that cannot be diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 7cc178f19df6f9..4d52bae904aa74 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -2,6 +2,7 @@ import importlib import io import itertools +lazy import json import os import pathlib import pkgutil @@ -1767,6 +1768,23 @@ def test_colorize_import_completions(self) -> None: ]) self.assertIsNone(action) + def test_find_attributes_uses_all(self): + """Test that _find_attributes respects __all__ when available.""" + completer = ModuleCompleter() + + # json module has __all__ defined + attrs, module, _ = completer._find_attributes('json', '') + + # Should match __all__ contents, not dir() which includes methods + expected = sorted(json.__all__) + self.assertEqual(sorted(attrs), expected) + + # Should NOT contain __all__ + self.assertNotIn('__all__', attrs) + + # Verify we got the actual module object + self.assertIs(module, sys.modules.get('json')) + # Audit hook used to check for stdlib modules import side-effects # Defined globally to avoid adding one hook per test run (refleak) From b6b138b4e4cfc5c7bcbbb96d626cd9156a834850 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 20 Aug 2026 22:37:30 -0700 Subject: [PATCH 2/8] Fix linting on tests --- Lib/test/test_pyrepl/test_pyrepl.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 4d52bae904aa74..27bd0b145fce54 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1771,17 +1771,13 @@ def test_colorize_import_completions(self) -> None: def test_find_attributes_uses_all(self): """Test that _find_attributes respects __all__ when available.""" completer = ModuleCompleter() - # json module has __all__ defined attrs, module, _ = completer._find_attributes('json', '') - # Should match __all__ contents, not dir() which includes methods expected = sorted(json.__all__) self.assertEqual(sorted(attrs), expected) - # Should NOT contain __all__ self.assertNotIn('__all__', attrs) - # Verify we got the actual module object self.assertIs(module, sys.modules.get('json')) From 690de0061adbb787168e5f493d2037d01e2d336f Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 05:46:18 +0000 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-08-21-05-46-17.gh-issue-149167.JcHCCr.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-08-21-05-46-17.gh-issue-149167.JcHCCr.rst diff --git a/Misc/NEWS.d/next/Library/2026-08-21-05-46-17.gh-issue-149167.JcHCCr.rst b/Misc/NEWS.d/next/Library/2026-08-21-05-46-17.gh-issue-149167.JcHCCr.rst new file mode 100644 index 00000000000000..eba7e0631bcb36 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-21-05-46-17.gh-issue-149167.JcHCCr.rst @@ -0,0 +1 @@ +Updates PyREPL autocomplete on import statements to only expose public members if declared with `__all__` From f41fbf0fff703cdeef8d0b4a9969a746911ab620 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 20 Aug 2026 22:50:30 -0700 Subject: [PATCH 4/8] Fix news entry --- .../next/Library/2026-08-21-05-46-17.gh-issue-149167.JcHCCr.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-08-21-05-46-17.gh-issue-149167.JcHCCr.rst b/Misc/NEWS.d/next/Library/2026-08-21-05-46-17.gh-issue-149167.JcHCCr.rst index eba7e0631bcb36..1e800c389d1902 100644 --- a/Misc/NEWS.d/next/Library/2026-08-21-05-46-17.gh-issue-149167.JcHCCr.rst +++ b/Misc/NEWS.d/next/Library/2026-08-21-05-46-17.gh-issue-149167.JcHCCr.rst @@ -1 +1 @@ -Updates PyREPL autocomplete on import statements to only expose public members if declared with `__all__` +Updates PyREPL autocomplete on import statements to only expose public members if declared with ``__all__`` From 62ddfc4b046e5a6a93ce96156e4f372e50ea5c9b Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 24 Sep 2026 09:33:30 -0700 Subject: [PATCH 5/8] Return __all__ directly if present --- 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 f19986704528fa..476ba0edacbb30 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -221,8 +221,8 @@ def _find_attributes( if not imported_module: return [], None, self._get_import_completion_action(path) try: - if hasattr(imported_module, '__all__'): # Use __all__ if available, otherwise use dir() - module_attributes = imported_module.__all__ + if hasattr(imported_module, '__all__'): # Return __all__ directly + return imported_module.__all__, imported_module, None else: module_attributes = dir(imported_module) except Exception: From ead3fbe755ba9119d2348d08ef14b842bafcc832 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 24 Sep 2026 09:39:35 -0700 Subject: [PATCH 6/8] Return __all__ directly if present --- Lib/_pyrepl/_module_completer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index 476ba0edacbb30..d56dcc71321c7e 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -222,7 +222,11 @@ def _find_attributes( return [], None, self._get_import_completion_action(path) try: if hasattr(imported_module, '__all__'): # Return __all__ directly - return imported_module.__all__, imported_module, None + names = [ + attr_name for attr_name in imported_module.__all__ + if attr_name.isidentifier() + ] + return names, imported_module, None else: module_attributes = dir(imported_module) except Exception: From c770b6b2eba6536d5b889bf1646918e401fe55f8 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 24 Sep 2026 09:44:14 -0700 Subject: [PATCH 7/8] Return __all__ directly if present --- Lib/_pyrepl/_module_completer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index d56dcc71321c7e..2505b041e76815 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -224,7 +224,7 @@ def _find_attributes( if hasattr(imported_module, '__all__'): # Return __all__ directly names = [ attr_name for attr_name in imported_module.__all__ - if attr_name.isidentifier() + if attr_name.startswith(prefix) and attr_name.isidentifier() ] return names, imported_module, None else: From 1525be8f3e37234f9467cb3c31980f57b8f3a576 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 24 Sep 2026 09:54:52 -0700 Subject: [PATCH 8/8] Add test --- Lib/test/test_pyrepl/test_pyrepl.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 27bd0b145fce54..6a5130112d3fd7 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -14,6 +14,7 @@ import tempfile from functools import partial from pkgutil import ModuleInfo +from types import ModuleType from unittest import TestCase, skipUnless, SkipTest from unittest.mock import Mock, patch import warnings @@ -1781,6 +1782,26 @@ def test_find_attributes_uses_all(self): # Verify we got the actual module object self.assertIs(module, sys.modules.get('json')) + def test_find_attributes_uses_all_with_private_names(self): + module = ModuleType("module_with_private_all") + module.__all__ = ["public", "_private", "not-valid"] + + completer = ModuleCompleter() + with patch.dict(sys.modules, {module.__name__: module}): + cases = ( + ("", ["public", "_private"]), + ("pub", ["public"]), + ("_", ["_private"]), + ) + for prefix, expected in cases: + with self.subTest(prefix=prefix): + attrs, actual_module, action = completer._find_attributes( + module.__name__, prefix + ) + self.assertEqual(attrs, expected) + self.assertIs(actual_module, module) + self.assertIsNone(action) + # Audit hook used to check for stdlib modules import side-effects # Defined globally to avoid adding one hook per test run (refleak)