##// END OF EJS Templates
Only complete exported submodules in import statements...
Daniel Shimon -
Show More
@@ -1,354 +1,370 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """Implementations for various useful completers.
2 """Implementations for various useful completers.
3
3
4 These are all loaded by default by IPython.
4 These are all loaded by default by IPython.
5 """
5 """
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Copyright (C) 2010-2011 The IPython Development Team.
7 # Copyright (C) 2010-2011 The IPython Development Team.
8 #
8 #
9 # Distributed under the terms of the BSD License.
9 # Distributed under the terms of the BSD License.
10 #
10 #
11 # The full license is in the file COPYING.txt, distributed with this software.
11 # The full license is in the file COPYING.txt, distributed with this software.
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 # Stdlib imports
18 # Stdlib imports
19 import glob
19 import glob
20 import inspect
20 import inspect
21 import os
21 import os
22 import re
22 import re
23 import sys
23 import sys
24 from importlib import import_module
24 from importlib import import_module
25 from importlib.machinery import all_suffixes
25 from importlib.machinery import all_suffixes
26
26
27
27
28 # Third-party imports
28 # Third-party imports
29 from time import time
29 from time import time
30 from zipimport import zipimporter
30 from zipimport import zipimporter
31
31
32 # Our own imports
32 # Our own imports
33 from .completer import expand_user, compress_user
33 from .completer import expand_user, compress_user
34 from .error import TryNext
34 from .error import TryNext
35 from ..utils._process_common import arg_split
35 from ..utils._process_common import arg_split
36
36
37 # FIXME: this should be pulled in with the right call via the component system
37 # FIXME: this should be pulled in with the right call via the component system
38 from IPython import get_ipython
38 from IPython import get_ipython
39
39
40 from typing import List
40 from typing import List
41
41
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43 # Globals and constants
43 # Globals and constants
44 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
45 _suffixes = all_suffixes()
45 _suffixes = all_suffixes()
46
46
47 # Time in seconds after which the rootmodules will be stored permanently in the
47 # Time in seconds after which the rootmodules will be stored permanently in the
48 # ipython ip.db database (kept in the user's .ipython dir).
48 # ipython ip.db database (kept in the user's .ipython dir).
49 TIMEOUT_STORAGE = 2
49 TIMEOUT_STORAGE = 2
50
50
51 # Time in seconds after which we give up
51 # Time in seconds after which we give up
52 TIMEOUT_GIVEUP = 20
52 TIMEOUT_GIVEUP = 20
53
53
54 # Regular expression for the python import statement
54 # Regular expression for the python import statement
55 import_re = re.compile(r'(?P<name>[^\W\d]\w*?)'
55 import_re = re.compile(r'(?P<name>[^\W\d]\w*?)'
56 r'(?P<package>[/\\]__init__)?'
56 r'(?P<package>[/\\]__init__)?'
57 r'(?P<suffix>%s)$' %
57 r'(?P<suffix>%s)$' %
58 r'|'.join(re.escape(s) for s in _suffixes))
58 r'|'.join(re.escape(s) for s in _suffixes))
59
59
60 # RE for the ipython %run command (python + ipython scripts)
60 # RE for the ipython %run command (python + ipython scripts)
61 magic_run_re = re.compile(r'.*(\.ipy|\.ipynb|\.py[w]?)$')
61 magic_run_re = re.compile(r'.*(\.ipy|\.ipynb|\.py[w]?)$')
62
62
63 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
64 # Local utilities
64 # Local utilities
65 #-----------------------------------------------------------------------------
65 #-----------------------------------------------------------------------------
66
66
67 def module_list(path):
67 def module_list(path):
68 """
68 """
69 Return the list containing the names of the modules available in the given
69 Return the list containing the names of the modules available in the given
70 folder.
70 folder.
71 """
71 """
72 # sys.path has the cwd as an empty string, but isdir/listdir need it as '.'
72 # sys.path has the cwd as an empty string, but isdir/listdir need it as '.'
73 if path == '':
73 if path == '':
74 path = '.'
74 path = '.'
75
75
76 # A few local constants to be used in loops below
76 # A few local constants to be used in loops below
77 pjoin = os.path.join
77 pjoin = os.path.join
78
78
79 if os.path.isdir(path):
79 if os.path.isdir(path):
80 # Build a list of all files in the directory and all files
80 # Build a list of all files in the directory and all files
81 # in its subdirectories. For performance reasons, do not
81 # in its subdirectories. For performance reasons, do not
82 # recurse more than one level into subdirectories.
82 # recurse more than one level into subdirectories.
83 files = []
83 files = []
84 for root, dirs, nondirs in os.walk(path, followlinks=True):
84 for root, dirs, nondirs in os.walk(path, followlinks=True):
85 subdir = root[len(path)+1:]
85 subdir = root[len(path)+1:]
86 if subdir:
86 if subdir:
87 files.extend(pjoin(subdir, f) for f in nondirs)
87 files.extend(pjoin(subdir, f) for f in nondirs)
88 dirs[:] = [] # Do not recurse into additional subdirectories.
88 dirs[:] = [] # Do not recurse into additional subdirectories.
89 else:
89 else:
90 files.extend(nondirs)
90 files.extend(nondirs)
91
91
92 else:
92 else:
93 try:
93 try:
94 files = list(zipimporter(path)._files.keys())
94 files = list(zipimporter(path)._files.keys())
95 except:
95 except:
96 files = []
96 files = []
97
97
98 # Build a list of modules which match the import_re regex.
98 # Build a list of modules which match the import_re regex.
99 modules = []
99 modules = []
100 for f in files:
100 for f in files:
101 m = import_re.match(f)
101 m = import_re.match(f)
102 if m:
102 if m:
103 modules.append(m.group('name'))
103 modules.append(m.group('name'))
104 return list(set(modules))
104 return list(set(modules))
105
105
106
106
107 def get_root_modules():
107 def get_root_modules():
108 """
108 """
109 Returns a list containing the names of all the modules available in the
109 Returns a list containing the names of all the modules available in the
110 folders of the pythonpath.
110 folders of the pythonpath.
111
111
112 ip.db['rootmodules_cache'] maps sys.path entries to list of modules.
112 ip.db['rootmodules_cache'] maps sys.path entries to list of modules.
113 """
113 """
114 ip = get_ipython()
114 ip = get_ipython()
115 if ip is None:
115 if ip is None:
116 # No global shell instance to store cached list of modules.
116 # No global shell instance to store cached list of modules.
117 # Don't try to scan for modules every time.
117 # Don't try to scan for modules every time.
118 return list(sys.builtin_module_names)
118 return list(sys.builtin_module_names)
119
119
120 rootmodules_cache = ip.db.get('rootmodules_cache', {})
120 rootmodules_cache = ip.db.get('rootmodules_cache', {})
121 rootmodules = list(sys.builtin_module_names)
121 rootmodules = list(sys.builtin_module_names)
122 start_time = time()
122 start_time = time()
123 store = False
123 store = False
124 for path in sys.path:
124 for path in sys.path:
125 try:
125 try:
126 modules = rootmodules_cache[path]
126 modules = rootmodules_cache[path]
127 except KeyError:
127 except KeyError:
128 modules = module_list(path)
128 modules = module_list(path)
129 try:
129 try:
130 modules.remove('__init__')
130 modules.remove('__init__')
131 except ValueError:
131 except ValueError:
132 pass
132 pass
133 if path not in ('', '.'): # cwd modules should not be cached
133 if path not in ('', '.'): # cwd modules should not be cached
134 rootmodules_cache[path] = modules
134 rootmodules_cache[path] = modules
135 if time() - start_time > TIMEOUT_STORAGE and not store:
135 if time() - start_time > TIMEOUT_STORAGE and not store:
136 store = True
136 store = True
137 print("\nCaching the list of root modules, please wait!")
137 print("\nCaching the list of root modules, please wait!")
138 print("(This will only be done once - type '%rehashx' to "
138 print("(This will only be done once - type '%rehashx' to "
139 "reset cache!)\n")
139 "reset cache!)\n")
140 sys.stdout.flush()
140 sys.stdout.flush()
141 if time() - start_time > TIMEOUT_GIVEUP:
141 if time() - start_time > TIMEOUT_GIVEUP:
142 print("This is taking too long, we give up.\n")
142 print("This is taking too long, we give up.\n")
143 return []
143 return []
144 rootmodules.extend(modules)
144 rootmodules.extend(modules)
145 if store:
145 if store:
146 ip.db['rootmodules_cache'] = rootmodules_cache
146 ip.db['rootmodules_cache'] = rootmodules_cache
147 rootmodules = list(set(rootmodules))
147 rootmodules = list(set(rootmodules))
148 return rootmodules
148 return rootmodules
149
149
150
150
151 def is_importable(module, attr, only_modules):
151 def is_importable(module, attr, only_modules):
152 if only_modules:
152 if only_modules:
153 return inspect.ismodule(getattr(module, attr))
153 return inspect.ismodule(getattr(module, attr))
154 else:
154 else:
155 return not(attr[:2] == '__' and attr[-2:] == '__')
155 return not(attr[:2] == '__' and attr[-2:] == '__')
156
156
157 def is_possible_submodule(module, attr):
158 try:
159 obj = getattr(module, attr)
160 except AttributeError:
161 # Is possilby an unimported submodule
162 return True
163 except TypeError:
164 # https://github.com/ipython/ipython/issues/9678
165 return False
166 return inspect.ismodule(obj)
167
157
168
158 def try_import(mod: str, only_modules=False) -> List[str]:
169 def try_import(mod: str, only_modules=False) -> List[str]:
159 """
170 """
160 Try to import given module and return list of potential completions.
171 Try to import given module and return list of potential completions.
161 """
172 """
162 mod = mod.rstrip('.')
173 mod = mod.rstrip('.')
163 try:
174 try:
164 m = import_module(mod)
175 m = import_module(mod)
165 except:
176 except:
166 return []
177 return []
167
178
168 m_is_init = '__init__' in (getattr(m, '__file__', '') or '')
179 m_is_init = '__init__' in (getattr(m, '__file__', '') or '')
169
180
170 completions = []
181 completions = []
171 if (not hasattr(m, '__file__')) or (not only_modules) or m_is_init:
182 if (not hasattr(m, '__file__')) or (not only_modules) or m_is_init:
172 completions.extend( [attr for attr in dir(m) if
183 completions.extend( [attr for attr in dir(m) if
173 is_importable(m, attr, only_modules)])
184 is_importable(m, attr, only_modules)])
174
185
175 completions.extend(getattr(m, '__all__', []))
186 m_all = getattr(m, "__all__", [])
187 if only_modules:
188 completions.extend(attr for attr in m_all if is_possible_submodule(m, attr))
189 else:
190 completions.extend(m_all)
191
176 if m_is_init:
192 if m_is_init:
177 completions.extend(module_list(os.path.dirname(m.__file__)))
193 completions.extend(module_list(os.path.dirname(m.__file__)))
178 completions_set = {c for c in completions if isinstance(c, str)}
194 completions_set = {c for c in completions if isinstance(c, str)}
179 completions_set.discard('__init__')
195 completions_set.discard('__init__')
180 return list(completions_set)
196 return list(completions_set)
181
197
182
198
183 #-----------------------------------------------------------------------------
199 #-----------------------------------------------------------------------------
184 # Completion-related functions.
200 # Completion-related functions.
185 #-----------------------------------------------------------------------------
201 #-----------------------------------------------------------------------------
186
202
187 def quick_completer(cmd, completions):
203 def quick_completer(cmd, completions):
188 r""" Easily create a trivial completer for a command.
204 r""" Easily create a trivial completer for a command.
189
205
190 Takes either a list of completions, or all completions in string (that will
206 Takes either a list of completions, or all completions in string (that will
191 be split on whitespace).
207 be split on whitespace).
192
208
193 Example::
209 Example::
194
210
195 [d:\ipython]|1> import ipy_completers
211 [d:\ipython]|1> import ipy_completers
196 [d:\ipython]|2> ipy_completers.quick_completer('foo', ['bar','baz'])
212 [d:\ipython]|2> ipy_completers.quick_completer('foo', ['bar','baz'])
197 [d:\ipython]|3> foo b<TAB>
213 [d:\ipython]|3> foo b<TAB>
198 bar baz
214 bar baz
199 [d:\ipython]|3> foo ba
215 [d:\ipython]|3> foo ba
200 """
216 """
201
217
202 if isinstance(completions, str):
218 if isinstance(completions, str):
203 completions = completions.split()
219 completions = completions.split()
204
220
205 def do_complete(self, event):
221 def do_complete(self, event):
206 return completions
222 return completions
207
223
208 get_ipython().set_hook('complete_command',do_complete, str_key = cmd)
224 get_ipython().set_hook('complete_command',do_complete, str_key = cmd)
209
225
210 def module_completion(line):
226 def module_completion(line):
211 """
227 """
212 Returns a list containing the completion possibilities for an import line.
228 Returns a list containing the completion possibilities for an import line.
213
229
214 The line looks like this :
230 The line looks like this :
215 'import xml.d'
231 'import xml.d'
216 'from xml.dom import'
232 'from xml.dom import'
217 """
233 """
218
234
219 words = line.split(' ')
235 words = line.split(' ')
220 nwords = len(words)
236 nwords = len(words)
221
237
222 # from whatever <tab> -> 'import '
238 # from whatever <tab> -> 'import '
223 if nwords == 3 and words[0] == 'from':
239 if nwords == 3 and words[0] == 'from':
224 return ['import ']
240 return ['import ']
225
241
226 # 'from xy<tab>' or 'import xy<tab>'
242 # 'from xy<tab>' or 'import xy<tab>'
227 if nwords < 3 and (words[0] in {'%aimport', 'import', 'from'}) :
243 if nwords < 3 and (words[0] in {'%aimport', 'import', 'from'}) :
228 if nwords == 1:
244 if nwords == 1:
229 return get_root_modules()
245 return get_root_modules()
230 mod = words[1].split('.')
246 mod = words[1].split('.')
231 if len(mod) < 2:
247 if len(mod) < 2:
232 return get_root_modules()
248 return get_root_modules()
233 completion_list = try_import('.'.join(mod[:-1]), True)
249 completion_list = try_import('.'.join(mod[:-1]), True)
234 return ['.'.join(mod[:-1] + [el]) for el in completion_list]
250 return ['.'.join(mod[:-1] + [el]) for el in completion_list]
235
251
236 # 'from xyz import abc<tab>'
252 # 'from xyz import abc<tab>'
237 if nwords >= 3 and words[0] == 'from':
253 if nwords >= 3 and words[0] == 'from':
238 mod = words[1]
254 mod = words[1]
239 return try_import(mod)
255 return try_import(mod)
240
256
241 #-----------------------------------------------------------------------------
257 #-----------------------------------------------------------------------------
242 # Completers
258 # Completers
243 #-----------------------------------------------------------------------------
259 #-----------------------------------------------------------------------------
244 # These all have the func(self, event) signature to be used as custom
260 # These all have the func(self, event) signature to be used as custom
245 # completers
261 # completers
246
262
247 def module_completer(self,event):
263 def module_completer(self,event):
248 """Give completions after user has typed 'import ...' or 'from ...'"""
264 """Give completions after user has typed 'import ...' or 'from ...'"""
249
265
250 # This works in all versions of python. While 2.5 has
266 # This works in all versions of python. While 2.5 has
251 # pkgutil.walk_packages(), that particular routine is fairly dangerous,
267 # pkgutil.walk_packages(), that particular routine is fairly dangerous,
252 # since it imports *EVERYTHING* on sys.path. That is: a) very slow b) full
268 # since it imports *EVERYTHING* on sys.path. That is: a) very slow b) full
253 # of possibly problematic side effects.
269 # of possibly problematic side effects.
254 # This search the folders in the sys.path for available modules.
270 # This search the folders in the sys.path for available modules.
255
271
256 return module_completion(event.line)
272 return module_completion(event.line)
257
273
258 # FIXME: there's a lot of logic common to the run, cd and builtin file
274 # FIXME: there's a lot of logic common to the run, cd and builtin file
259 # completers, that is currently reimplemented in each.
275 # completers, that is currently reimplemented in each.
260
276
261 def magic_run_completer(self, event):
277 def magic_run_completer(self, event):
262 """Complete files that end in .py or .ipy or .ipynb for the %run command.
278 """Complete files that end in .py or .ipy or .ipynb for the %run command.
263 """
279 """
264 comps = arg_split(event.line, strict=False)
280 comps = arg_split(event.line, strict=False)
265 # relpath should be the current token that we need to complete.
281 # relpath should be the current token that we need to complete.
266 if (len(comps) > 1) and (not event.line.endswith(' ')):
282 if (len(comps) > 1) and (not event.line.endswith(' ')):
267 relpath = comps[-1].strip("'\"")
283 relpath = comps[-1].strip("'\"")
268 else:
284 else:
269 relpath = ''
285 relpath = ''
270
286
271 #print("\nev=", event) # dbg
287 #print("\nev=", event) # dbg
272 #print("rp=", relpath) # dbg
288 #print("rp=", relpath) # dbg
273 #print('comps=', comps) # dbg
289 #print('comps=', comps) # dbg
274
290
275 lglob = glob.glob
291 lglob = glob.glob
276 isdir = os.path.isdir
292 isdir = os.path.isdir
277 relpath, tilde_expand, tilde_val = expand_user(relpath)
293 relpath, tilde_expand, tilde_val = expand_user(relpath)
278
294
279 # Find if the user has already typed the first filename, after which we
295 # Find if the user has already typed the first filename, after which we
280 # should complete on all files, since after the first one other files may
296 # should complete on all files, since after the first one other files may
281 # be arguments to the input script.
297 # be arguments to the input script.
282
298
283 if any(magic_run_re.match(c) for c in comps):
299 if any(magic_run_re.match(c) for c in comps):
284 matches = [f.replace('\\','/') + ('/' if isdir(f) else '')
300 matches = [f.replace('\\','/') + ('/' if isdir(f) else '')
285 for f in lglob(relpath+'*')]
301 for f in lglob(relpath+'*')]
286 else:
302 else:
287 dirs = [f.replace('\\','/') + "/" for f in lglob(relpath+'*') if isdir(f)]
303 dirs = [f.replace('\\','/') + "/" for f in lglob(relpath+'*') if isdir(f)]
288 pys = [f.replace('\\','/')
304 pys = [f.replace('\\','/')
289 for f in lglob(relpath+'*.py') + lglob(relpath+'*.ipy') +
305 for f in lglob(relpath+'*.py') + lglob(relpath+'*.ipy') +
290 lglob(relpath+'*.ipynb') + lglob(relpath + '*.pyw')]
306 lglob(relpath+'*.ipynb') + lglob(relpath + '*.pyw')]
291
307
292 matches = dirs + pys
308 matches = dirs + pys
293
309
294 #print('run comp:', dirs+pys) # dbg
310 #print('run comp:', dirs+pys) # dbg
295 return [compress_user(p, tilde_expand, tilde_val) for p in matches]
311 return [compress_user(p, tilde_expand, tilde_val) for p in matches]
296
312
297
313
298 def cd_completer(self, event):
314 def cd_completer(self, event):
299 """Completer function for cd, which only returns directories."""
315 """Completer function for cd, which only returns directories."""
300 ip = get_ipython()
316 ip = get_ipython()
301 relpath = event.symbol
317 relpath = event.symbol
302
318
303 #print(event) # dbg
319 #print(event) # dbg
304 if event.line.endswith('-b') or ' -b ' in event.line:
320 if event.line.endswith('-b') or ' -b ' in event.line:
305 # return only bookmark completions
321 # return only bookmark completions
306 bkms = self.db.get('bookmarks', None)
322 bkms = self.db.get('bookmarks', None)
307 if bkms:
323 if bkms:
308 return bkms.keys()
324 return bkms.keys()
309 else:
325 else:
310 return []
326 return []
311
327
312 if event.symbol == '-':
328 if event.symbol == '-':
313 width_dh = str(len(str(len(ip.user_ns['_dh']) + 1)))
329 width_dh = str(len(str(len(ip.user_ns['_dh']) + 1)))
314 # jump in directory history by number
330 # jump in directory history by number
315 fmt = '-%0' + width_dh +'d [%s]'
331 fmt = '-%0' + width_dh +'d [%s]'
316 ents = [ fmt % (i,s) for i,s in enumerate(ip.user_ns['_dh'])]
332 ents = [ fmt % (i,s) for i,s in enumerate(ip.user_ns['_dh'])]
317 if len(ents) > 1:
333 if len(ents) > 1:
318 return ents
334 return ents
319 return []
335 return []
320
336
321 if event.symbol.startswith('--'):
337 if event.symbol.startswith('--'):
322 return ["--" + os.path.basename(d) for d in ip.user_ns['_dh']]
338 return ["--" + os.path.basename(d) for d in ip.user_ns['_dh']]
323
339
324 # Expand ~ in path and normalize directory separators.
340 # Expand ~ in path and normalize directory separators.
325 relpath, tilde_expand, tilde_val = expand_user(relpath)
341 relpath, tilde_expand, tilde_val = expand_user(relpath)
326 relpath = relpath.replace('\\','/')
342 relpath = relpath.replace('\\','/')
327
343
328 found = []
344 found = []
329 for d in [f.replace('\\','/') + '/' for f in glob.glob(relpath+'*')
345 for d in [f.replace('\\','/') + '/' for f in glob.glob(relpath+'*')
330 if os.path.isdir(f)]:
346 if os.path.isdir(f)]:
331 if ' ' in d:
347 if ' ' in d:
332 # we don't want to deal with any of that, complex code
348 # we don't want to deal with any of that, complex code
333 # for this is elsewhere
349 # for this is elsewhere
334 raise TryNext
350 raise TryNext
335
351
336 found.append(d)
352 found.append(d)
337
353
338 if not found:
354 if not found:
339 if os.path.isdir(relpath):
355 if os.path.isdir(relpath):
340 return [compress_user(relpath, tilde_expand, tilde_val)]
356 return [compress_user(relpath, tilde_expand, tilde_val)]
341
357
342 # if no completions so far, try bookmarks
358 # if no completions so far, try bookmarks
343 bks = self.db.get('bookmarks',{})
359 bks = self.db.get('bookmarks',{})
344 bkmatches = [s for s in bks if s.startswith(event.symbol)]
360 bkmatches = [s for s in bks if s.startswith(event.symbol)]
345 if bkmatches:
361 if bkmatches:
346 return bkmatches
362 return bkmatches
347
363
348 raise TryNext
364 raise TryNext
349
365
350 return [compress_user(p, tilde_expand, tilde_val) for p in found]
366 return [compress_user(p, tilde_expand, tilde_val) for p in found]
351
367
352 def reset_completer(self, event):
368 def reset_completer(self, event):
353 "A completer for %reset magic"
369 "A completer for %reset magic"
354 return '-f -s in out array dhist'.split()
370 return '-f -s in out array dhist'.split()
General Comments 0
You need to be logged in to leave comments. Login now