1717
1818if TYPE_CHECKING :
1919 from typing import Any , Iterable , Iterator , Mapping
20+ from .types import CompletionAction
2021
2122
2223HARDCODED_SUBMODULES = {
@@ -52,11 +53,17 @@ class ModuleCompleter:
5253 def __init__ (self , namespace : Mapping [str , Any ] | None = None ) -> None :
5354 self .namespace = namespace or {}
5455 self ._global_cache : list [pkgutil .ModuleInfo ] = []
56+ self ._failed_imports : set [str ] = set ()
5557 self ._curr_sys_path : list [str ] = sys .path [:]
5658 self ._stdlib_path = os .path .dirname (importlib .__path__ [0 ])
5759
58- def get_completions (self , line : str ) -> list [str ] | None :
59- """Return the next possible import completions for 'line'."""
60+ def get_completions (self , line : str ) -> tuple [list [str ], CompletionAction | None ] | None :
61+ """Return the next possible import completions for 'line'.
62+
63+ For attributes completion, if the module to complete from is not
64+ imported, also return an action (prompt + callback to run if the
65+ user press TAB again) to import the module.
66+ """
6067 result = ImportParser (line ).parse ()
6168 if not result :
6269 return None
@@ -65,24 +72,26 @@ def get_completions(self, line: str) -> list[str] | None:
6572 except Exception :
6673 # Some unexpected error occurred, make it look like
6774 # no completions are available
68- return []
75+ return [], None
6976
70- def complete (self , from_name : str | None , name : str | None ) -> list [str ]:
77+ def complete (self , from_name : str | None , name : str | None ) -> tuple [ list [str ], CompletionAction | None ]:
7178 if from_name is None :
7279 # import x.y.z<tab>
7380 assert name is not None
7481 path , prefix = self .get_path_and_prefix (name )
7582 modules = self .find_modules (path , prefix )
76- return [self .format_completion (path , module ) for module in modules ]
83+ return [self .format_completion (path , module ) for module in modules ], None
7784
7885 if name is None :
7986 # from x.y.z<tab>
8087 path , prefix = self .get_path_and_prefix (from_name )
8188 modules = self .find_modules (path , prefix )
82- return [self .format_completion (path , module ) for module in modules ]
89+ return [self .format_completion (path , module ) for module in modules ], None
8390
8491 # from x.y import z<tab>
85- return self .find_modules (from_name , name )
92+ submodules = self .find_modules (from_name , name )
93+ attributes , action = self .find_attributes (from_name , name )
94+ return sorted ({* submodules , * attributes }), action
8695
8796 def find_modules (self , path : str , prefix : str ) -> list [str ]:
8897 """Find all modules under 'path' that start with 'prefix'."""
@@ -129,6 +138,33 @@ def _is_stdlib_module(self, module_info: pkgutil.ModuleInfo) -> bool:
129138 return (isinstance (module_info .module_finder , FileFinder )
130139 and module_info .module_finder .path == self ._stdlib_path )
131140
141+ def find_attributes (self , path : str , prefix : str ) -> tuple [list [str ], CompletionAction | None ]:
142+ """Find all attributes of module 'path' that start with 'prefix'."""
143+ attributes , action = self ._find_attributes (path , prefix )
144+ # Filter out invalid attribute names
145+ # (for example those containing dashes that cannot be imported with 'import')
146+ return [attr for attr in attributes if attr .isidentifier ()], action
147+
148+ def _find_attributes (self , path : str , prefix : str ) -> tuple [list [str ], CompletionAction | None ]:
149+ if path .startswith ('.' ):
150+ # Convert relative path to absolute path
151+ package = self .namespace .get ('__package__' , '' )
152+ path = self .resolve_relative_name (path , package ) # type: ignore[assignment]
153+ if path is None :
154+ return [], None
155+
156+ imported_module = sys .modules .get (path )
157+ if not imported_module :
158+ if path in self ._failed_imports : # Do not propose to import again
159+ return [], None
160+ return [], self ._get_import_completion_action (path )
161+ try :
162+ module_attributes = dir (imported_module )
163+ except Exception :
164+ module_attributes = []
165+ return [attr_name for attr_name in module_attributes
166+ if self .is_suggestion_match (attr_name , prefix )], None
167+
132168 def is_suggestion_match (self , module_name : str , prefix : str ) -> bool :
133169 if prefix :
134170 return module_name .startswith (prefix )
@@ -200,6 +236,21 @@ def global_cache(self) -> list[pkgutil.ModuleInfo]:
200236 self ._global_cache = list (pkgutil .iter_modules ())
201237 return self ._global_cache
202238
239+ def _get_import_completion_action (self , path : str ) -> CompletionAction :
240+ prompt = ("[ module not imported, press again to import it "
241+ "and propose attributes ]" )
242+
243+ def _do_import () -> str | None :
244+ try :
245+ importlib .import_module (path )
246+ return None
247+ except Exception as exc :
248+ sys .modules .pop (path , None ) # Clean half-imported module
249+ self ._failed_imports .add (path )
250+ return f"[ error during import: { exc } ]"
251+
252+ return (prompt , _do_import )
253+
203254
204255class ImportParser :
205256 """
0 commit comments