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