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