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