diff --git a/Lib/_pyrepl/_module_completer.py b/Lib/_pyrepl/_module_completer.py index 17bf5cdc819542d..2505b041e76815a 100644 --- a/Lib/_pyrepl/_module_completer.py +++ b/Lib/_pyrepl/_module_completer.py @@ -221,7 +221,14 @@ 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__'): # Return __all__ directly + names = [ + attr_name for attr_name in imported_module.__all__ + if attr_name.startswith(prefix) and attr_name.isidentifier() + ] + return names, imported_module, None + 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 7cc178f19df6f9d..6a5130112d3fd70 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 @@ -13,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 @@ -1767,6 +1769,39 @@ 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')) + + 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) 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 000000000000000..1e800c389d1902d --- /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__``