mirror of
https://github.com/python/cpython.git
synced 2025-11-24 12:20:42 +00:00
[3.14] gh-69605: Hardcode some stdlib submodules in PyREPL module completion (os.path, collections.abc...) (GH-138268) (GH-138943)
(cherry picked from commit 537133d2b6)
Co-authored-by: Loïc Simon <loic.simon@napta.io>
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
This commit is contained in:
parent
cde02ae782
commit
d912e9a852
3 changed files with 80 additions and 14 deletions
|
|
@ -1,9 +1,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import sys
|
import sys
|
||||||
import token
|
import token
|
||||||
import tokenize
|
import tokenize
|
||||||
|
from importlib.machinery import FileFinder
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
@ -16,6 +19,15 @@ if TYPE_CHECKING:
|
||||||
from typing import Any, Iterable, Iterator, Mapping
|
from typing import Any, Iterable, Iterator, Mapping
|
||||||
|
|
||||||
|
|
||||||
|
HARDCODED_SUBMODULES = {
|
||||||
|
# Standard library submodules that are not detected by pkgutil.iter_modules
|
||||||
|
# but can be imported, so should be proposed in completion
|
||||||
|
"collections": ["abc"],
|
||||||
|
"os": ["path"],
|
||||||
|
"xml.parsers.expat": ["errors", "model"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def make_default_module_completer() -> ModuleCompleter:
|
def make_default_module_completer() -> ModuleCompleter:
|
||||||
# Inside pyrepl, __package__ is set to None by default
|
# Inside pyrepl, __package__ is set to None by default
|
||||||
return ModuleCompleter(namespace={'__package__': None})
|
return ModuleCompleter(namespace={'__package__': None})
|
||||||
|
|
@ -41,6 +53,7 @@ class ModuleCompleter:
|
||||||
self.namespace = namespace or {}
|
self.namespace = namespace or {}
|
||||||
self._global_cache: list[pkgutil.ModuleInfo] = []
|
self._global_cache: list[pkgutil.ModuleInfo] = []
|
||||||
self._curr_sys_path: list[str] = sys.path[:]
|
self._curr_sys_path: list[str] = sys.path[:]
|
||||||
|
self._stdlib_path = os.path.dirname(importlib.__path__[0])
|
||||||
|
|
||||||
def get_completions(self, line: str) -> list[str] | None:
|
def get_completions(self, line: str) -> list[str] | None:
|
||||||
"""Return the next possible import completions for 'line'."""
|
"""Return the next possible import completions for 'line'."""
|
||||||
|
|
@ -95,12 +108,26 @@ class ModuleCompleter:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
modules: Iterable[pkgutil.ModuleInfo] = self.global_cache
|
modules: Iterable[pkgutil.ModuleInfo] = self.global_cache
|
||||||
|
is_stdlib_import: bool | None = None
|
||||||
for segment in path.split('.'):
|
for segment in path.split('.'):
|
||||||
modules = [mod_info for mod_info in modules
|
modules = [mod_info for mod_info in modules
|
||||||
if mod_info.ispkg and mod_info.name == segment]
|
if mod_info.ispkg and mod_info.name == segment]
|
||||||
|
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 = self.iter_submodules(modules)
|
||||||
return [module.name for module in modules
|
|
||||||
if self.is_suggestion_match(module.name, prefix)]
|
module_names = [module.name for module in modules]
|
||||||
|
if is_stdlib_import:
|
||||||
|
module_names.extend(HARDCODED_SUBMODULES.get(path, ()))
|
||||||
|
return [module_name for module_name in module_names
|
||||||
|
if self.is_suggestion_match(module_name, prefix)]
|
||||||
|
|
||||||
|
def _is_stdlib_module(self, module_info: pkgutil.ModuleInfo) -> bool:
|
||||||
|
return (isinstance(module_info.module_finder, FileFinder)
|
||||||
|
and module_info.module_finder.path == self._stdlib_path)
|
||||||
|
|
||||||
def is_suggestion_match(self, module_name: str, prefix: str) -> bool:
|
def is_suggestion_match(self, module_name: str, prefix: str) -> bool:
|
||||||
if prefix:
|
if prefix:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import importlib
|
||||||
import io
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
|
|
@ -26,9 +27,16 @@ from .support import (
|
||||||
code_to_events,
|
code_to_events,
|
||||||
)
|
)
|
||||||
from _pyrepl.console import Event
|
from _pyrepl.console import Event
|
||||||
from _pyrepl._module_completer import ImportParser, ModuleCompleter
|
from _pyrepl._module_completer import (
|
||||||
from _pyrepl.readline import (ReadlineAlikeReader, ReadlineConfig,
|
ImportParser,
|
||||||
_ReadlineWrapper)
|
ModuleCompleter,
|
||||||
|
HARDCODED_SUBMODULES,
|
||||||
|
)
|
||||||
|
from _pyrepl.readline import (
|
||||||
|
ReadlineAlikeReader,
|
||||||
|
ReadlineConfig,
|
||||||
|
_ReadlineWrapper,
|
||||||
|
)
|
||||||
from _pyrepl.readline import multiline_input as readline_multiline_input
|
from _pyrepl.readline import multiline_input as readline_multiline_input
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -930,7 +938,6 @@ class TestPyReplCompleter(TestCase):
|
||||||
|
|
||||||
class TestPyReplModuleCompleter(TestCase):
|
class TestPyReplModuleCompleter(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
import importlib
|
|
||||||
# Make iter_modules() search only the standard library.
|
# Make iter_modules() search only the standard library.
|
||||||
# This makes the test more reliable in case there are
|
# This makes the test more reliable in case there are
|
||||||
# other user packages/scripts on PYTHONPATH which can
|
# other user packages/scripts on PYTHONPATH which can
|
||||||
|
|
@ -1013,14 +1020,6 @@ class TestPyReplModuleCompleter(TestCase):
|
||||||
self.assertEqual(output, expected)
|
self.assertEqual(output, expected)
|
||||||
|
|
||||||
def test_builtin_completion_top_level(self):
|
def test_builtin_completion_top_level(self):
|
||||||
import importlib
|
|
||||||
# Make iter_modules() search only the standard library.
|
|
||||||
# This makes the test more reliable in case there are
|
|
||||||
# other user packages/scripts on PYTHONPATH which can
|
|
||||||
# intefere with the completions.
|
|
||||||
lib_path = os.path.dirname(importlib.__path__[0])
|
|
||||||
sys.path = [lib_path]
|
|
||||||
|
|
||||||
cases = (
|
cases = (
|
||||||
("import bui\t\n", "import builtins"),
|
("import bui\t\n", "import builtins"),
|
||||||
("from bui\t\n", "from builtins"),
|
("from bui\t\n", "from builtins"),
|
||||||
|
|
@ -1076,6 +1075,32 @@ class TestPyReplModuleCompleter(TestCase):
|
||||||
output = reader.readline()
|
output = reader.readline()
|
||||||
self.assertEqual(output, expected)
|
self.assertEqual(output, expected)
|
||||||
|
|
||||||
|
def test_hardcoded_stdlib_submodules(self):
|
||||||
|
cases = (
|
||||||
|
("import collections.\t\n", "import collections.abc"),
|
||||||
|
("from os import \t\n", "from os import path"),
|
||||||
|
("import xml.parsers.expat.\t\te\t\n\n", "import xml.parsers.expat.errors"),
|
||||||
|
("from xml.parsers.expat import \t\tm\t\n\n", "from xml.parsers.expat import model"),
|
||||||
|
)
|
||||||
|
for code, expected in cases:
|
||||||
|
with self.subTest(code=code):
|
||||||
|
events = code_to_events(code)
|
||||||
|
reader = self.prepare_reader(events, namespace={})
|
||||||
|
output = reader.readline()
|
||||||
|
self.assertEqual(output, expected)
|
||||||
|
|
||||||
|
def test_hardcoded_stdlib_submodules_not_proposed_if_local_import(self):
|
||||||
|
with tempfile.TemporaryDirectory() as _dir:
|
||||||
|
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]):
|
||||||
|
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_get_path_and_prefix(self):
|
def test_get_path_and_prefix(self):
|
||||||
cases = (
|
cases = (
|
||||||
('', ('', '')),
|
('', ('', '')),
|
||||||
|
|
@ -1204,6 +1229,19 @@ class TestPyReplModuleCompleter(TestCase):
|
||||||
with self.subTest(code=code):
|
with self.subTest(code=code):
|
||||||
self.assertEqual(actual, None)
|
self.assertEqual(actual, None)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHardcodedSubmodules(TestCase):
|
||||||
|
def test_hardcoded_stdlib_submodules_are_importable(self):
|
||||||
|
for parent_path, submodules in HARDCODED_SUBMODULES.items():
|
||||||
|
for module_name in submodules:
|
||||||
|
path = f"{parent_path}.{module_name}"
|
||||||
|
with self.subTest(path=path):
|
||||||
|
# We can't use importlib.util.find_spec here,
|
||||||
|
# since some hardcoded submodules parents are
|
||||||
|
# not proper packages
|
||||||
|
importlib.import_module(path)
|
||||||
|
|
||||||
|
|
||||||
class TestPasteEvent(TestCase):
|
class TestPasteEvent(TestCase):
|
||||||
def prepare_reader(self, events):
|
def prepare_reader(self, events):
|
||||||
console = FakeConsole(events)
|
console = FakeConsole(events)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Fix some standard library submodules missing from the :term:`REPL` auto-completion of imports.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue