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