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