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