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