##// END OF EJS Templates
Merge pull request #13069 from daniel-shimon/exported-submodules-completion...
Matthias Bussonnier -
r26708:6a1e9d0d merge
parent child Browse files
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()
@@ -1,178 +1,194 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Tests for completerlib.
3 3
4 4 """
5 5
6 6 #-----------------------------------------------------------------------------
7 7 # Imports
8 8 #-----------------------------------------------------------------------------
9 9
10 10 import os
11 11 import shutil
12 12 import sys
13 13 import tempfile
14 14 import unittest
15 15 from os.path import join
16 16
17 17 import nose.tools as nt
18 18
19 19 from IPython.core.completerlib import magic_run_completer, module_completion, try_import
20 20 from IPython.utils.tempdir import TemporaryDirectory
21 21 from IPython.testing.decorators import onlyif_unicode_paths
22 22
23 23
24 24 class MockEvent(object):
25 25 def __init__(self, line):
26 26 self.line = line
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Test functions begin
30 30 #-----------------------------------------------------------------------------
31 31 class Test_magic_run_completer(unittest.TestCase):
32 32 files = [u"aao.py", u"a.py", u"b.py", u"aao.txt"]
33 33 dirs = [u"adir/", "bdir/"]
34 34
35 35 def setUp(self):
36 36 self.BASETESTDIR = tempfile.mkdtemp()
37 37 for fil in self.files:
38 38 with open(join(self.BASETESTDIR, fil), "w") as sfile:
39 39 sfile.write("pass\n")
40 40 for d in self.dirs:
41 41 os.mkdir(join(self.BASETESTDIR, d))
42 42
43 43 self.oldpath = os.getcwd()
44 44 os.chdir(self.BASETESTDIR)
45 45
46 46 def tearDown(self):
47 47 os.chdir(self.oldpath)
48 48 shutil.rmtree(self.BASETESTDIR)
49 49
50 50 def test_1(self):
51 51 """Test magic_run_completer, should match two alternatives
52 52 """
53 53 event = MockEvent(u"%run a")
54 54 mockself = None
55 55 match = set(magic_run_completer(mockself, event))
56 56 self.assertEqual(match, {u"a.py", u"aao.py", u"adir/"})
57 57
58 58 def test_2(self):
59 59 """Test magic_run_completer, should match one alternative
60 60 """
61 61 event = MockEvent(u"%run aa")
62 62 mockself = None
63 63 match = set(magic_run_completer(mockself, event))
64 64 self.assertEqual(match, {u"aao.py"})
65 65
66 66 def test_3(self):
67 67 """Test magic_run_completer with unterminated " """
68 68 event = MockEvent(u'%run "a')
69 69 mockself = None
70 70 match = set(magic_run_completer(mockself, event))
71 71 self.assertEqual(match, {u"a.py", u"aao.py", u"adir/"})
72 72
73 73 def test_completion_more_args(self):
74 74 event = MockEvent(u'%run a.py ')
75 75 match = set(magic_run_completer(None, event))
76 76 self.assertEqual(match, set(self.files + self.dirs))
77 77
78 78 def test_completion_in_dir(self):
79 79 # Github issue #3459
80 80 event = MockEvent(u'%run a.py {}'.format(join(self.BASETESTDIR, 'a')))
81 81 print(repr(event.line))
82 82 match = set(magic_run_completer(None, event))
83 83 # We specifically use replace here rather than normpath, because
84 84 # at one point there were duplicates 'adir' and 'adir/', and normpath
85 85 # would hide the failure for that.
86 86 self.assertEqual(match, {join(self.BASETESTDIR, f).replace('\\','/')
87 87 for f in (u'a.py', u'aao.py', u'aao.txt', u'adir/')})
88 88
89 89 class Test_magic_run_completer_nonascii(unittest.TestCase):
90 90 @onlyif_unicode_paths
91 91 def setUp(self):
92 92 self.BASETESTDIR = tempfile.mkdtemp()
93 93 for fil in [u"aaΓΈ.py", u"a.py", u"b.py"]:
94 94 with open(join(self.BASETESTDIR, fil), "w") as sfile:
95 95 sfile.write("pass\n")
96 96 self.oldpath = os.getcwd()
97 97 os.chdir(self.BASETESTDIR)
98 98
99 99 def tearDown(self):
100 100 os.chdir(self.oldpath)
101 101 shutil.rmtree(self.BASETESTDIR)
102 102
103 103 @onlyif_unicode_paths
104 104 def test_1(self):
105 105 """Test magic_run_completer, should match two alternatives
106 106 """
107 107 event = MockEvent(u"%run a")
108 108 mockself = None
109 109 match = set(magic_run_completer(mockself, event))
110 110 self.assertEqual(match, {u"a.py", u"aaΓΈ.py"})
111 111
112 112 @onlyif_unicode_paths
113 113 def test_2(self):
114 114 """Test magic_run_completer, should match one alternative
115 115 """
116 116 event = MockEvent(u"%run aa")
117 117 mockself = None
118 118 match = set(magic_run_completer(mockself, event))
119 119 self.assertEqual(match, {u"aaΓΈ.py"})
120 120
121 121 @onlyif_unicode_paths
122 122 def test_3(self):
123 123 """Test magic_run_completer with unterminated " """
124 124 event = MockEvent(u'%run "a')
125 125 mockself = None
126 126 match = set(magic_run_completer(mockself, event))
127 127 self.assertEqual(match, {u"a.py", u"aaΓΈ.py"})
128 128
129 129 # module_completer:
130 130
131 131 def test_import_invalid_module():
132 132 """Testing of issue https://github.com/ipython/ipython/issues/1107"""
133 133 invalid_module_names = {'foo-bar', 'foo:bar', '10foo'}
134 134 valid_module_names = {'foobar'}
135 135 with TemporaryDirectory() as tmpdir:
136 136 sys.path.insert( 0, tmpdir )
137 137 for name in invalid_module_names | valid_module_names:
138 138 filename = os.path.join(tmpdir, name + '.py')
139 139 open(filename, 'w').close()
140 140
141 141 s = set( module_completion('import foo') )
142 142 intersection = s.intersection(invalid_module_names)
143 143 nt.assert_equal(intersection, set())
144 144
145 145 assert valid_module_names.issubset(s), valid_module_names.intersection(s)
146 146
147 147
148 148 def test_bad_module_all():
149 149 """Test module with invalid __all__
150 150
151 151 https://github.com/ipython/ipython/issues/9678
152 152 """
153 153 testsdir = os.path.dirname(__file__)
154 154 sys.path.insert(0, testsdir)
155 155 try:
156 156 results = module_completion('from bad_all import ')
157 157 nt.assert_in('puppies', results)
158 158 for r in results:
159 159 nt.assert_is_instance(r, str)
160
161 # bad_all doesn't contain submodules, but this completion
162 # should finish without raising an exception:
163 results = module_completion("import bad_all.")
164 nt.assert_equal(results, [])
160 165 finally:
161 166 sys.path.remove(testsdir)
162 167
163 168
164 169 def test_module_without_init():
165 170 """
166 171 Test module without __init__.py.
167 172
168 173 https://github.com/ipython/ipython/issues/11226
169 174 """
170 175 fake_module_name = "foo"
171 176 with TemporaryDirectory() as tmpdir:
172 177 sys.path.insert(0, tmpdir)
173 178 try:
174 179 os.makedirs(os.path.join(tmpdir, fake_module_name))
175 180 s = try_import(mod=fake_module_name)
176 181 assert s == []
177 182 finally:
178 183 sys.path.remove(tmpdir)
184
185
186 def test_valid_exported_submodules():
187 """
188 Test checking exported (__all__) objects are submodules
189 """
190 results = module_completion("import os.pa")
191 # ensure we get a valid submodule:
192 nt.assert_in("os.path", results)
193 # ensure we don't get objects that aren't submodules:
194 nt.assert_not_in("os.pathconf", results)
General Comments 0
You need to be logged in to leave comments. Login now