##// END OF EJS Templates
Merge pull request #11233 from meeseeksmachine/auto-backport-of-pr-11227-on-6.x...
Matthias Bussonnier -
r24432:cfd7d5f4 merge
parent child Browse files
Show More
@@ -1,354 +1,354 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 IPython.core.completer import expand_user, compress_user
34 34 from IPython.core.error import TryNext
35 35 from IPython.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>[a-zA-Z_][a-zA-Z0-9_]*?)'
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 157
158 158 def try_import(mod: str, only_modules=False) -> List[str]:
159 159 """
160 160 Try to import given module and return list of potential completions.
161 161 """
162 162 mod = mod.rstrip('.')
163 163 try:
164 164 m = import_module(mod)
165 165 except:
166 166 return []
167 167
168 m_is_init = hasattr(m, '__file__') and '__init__' in m.__file__
168 m_is_init = '__init__' in (getattr(m, '__file__', '') or '')
169 169
170 170 completions = []
171 171 if (not hasattr(m, '__file__')) or (not only_modules) or m_is_init:
172 172 completions.extend( [attr for attr in dir(m) if
173 173 is_importable(m, attr, only_modules)])
174 174
175 175 completions.extend(getattr(m, '__all__', []))
176 176 if m_is_init:
177 177 completions.extend(module_list(os.path.dirname(m.__file__)))
178 178 completions_set = {c for c in completions if isinstance(c, str)}
179 179 completions_set.discard('__init__')
180 180 return list(completions_set)
181 181
182 182
183 183 #-----------------------------------------------------------------------------
184 184 # Completion-related functions.
185 185 #-----------------------------------------------------------------------------
186 186
187 187 def quick_completer(cmd, completions):
188 188 """ Easily create a trivial completer for a command.
189 189
190 190 Takes either a list of completions, or all completions in string (that will
191 191 be split on whitespace).
192 192
193 193 Example::
194 194
195 195 [d:\ipython]|1> import ipy_completers
196 196 [d:\ipython]|2> ipy_completers.quick_completer('foo', ['bar','baz'])
197 197 [d:\ipython]|3> foo b<TAB>
198 198 bar baz
199 199 [d:\ipython]|3> foo ba
200 200 """
201 201
202 202 if isinstance(completions, str):
203 203 completions = completions.split()
204 204
205 205 def do_complete(self, event):
206 206 return completions
207 207
208 208 get_ipython().set_hook('complete_command',do_complete, str_key = cmd)
209 209
210 210 def module_completion(line):
211 211 """
212 212 Returns a list containing the completion possibilities for an import line.
213 213
214 214 The line looks like this :
215 215 'import xml.d'
216 216 'from xml.dom import'
217 217 """
218 218
219 219 words = line.split(' ')
220 220 nwords = len(words)
221 221
222 222 # from whatever <tab> -> 'import '
223 223 if nwords == 3 and words[0] == 'from':
224 224 return ['import ']
225 225
226 226 # 'from xy<tab>' or 'import xy<tab>'
227 227 if nwords < 3 and (words[0] in {'%aimport', 'import', 'from'}) :
228 228 if nwords == 1:
229 229 return get_root_modules()
230 230 mod = words[1].split('.')
231 231 if len(mod) < 2:
232 232 return get_root_modules()
233 233 completion_list = try_import('.'.join(mod[:-1]), True)
234 234 return ['.'.join(mod[:-1] + [el]) for el in completion_list]
235 235
236 236 # 'from xyz import abc<tab>'
237 237 if nwords >= 3 and words[0] == 'from':
238 238 mod = words[1]
239 239 return try_import(mod)
240 240
241 241 #-----------------------------------------------------------------------------
242 242 # Completers
243 243 #-----------------------------------------------------------------------------
244 244 # These all have the func(self, event) signature to be used as custom
245 245 # completers
246 246
247 247 def module_completer(self,event):
248 248 """Give completions after user has typed 'import ...' or 'from ...'"""
249 249
250 250 # This works in all versions of python. While 2.5 has
251 251 # pkgutil.walk_packages(), that particular routine is fairly dangerous,
252 252 # since it imports *EVERYTHING* on sys.path. That is: a) very slow b) full
253 253 # of possibly problematic side effects.
254 254 # This search the folders in the sys.path for available modules.
255 255
256 256 return module_completion(event.line)
257 257
258 258 # FIXME: there's a lot of logic common to the run, cd and builtin file
259 259 # completers, that is currently reimplemented in each.
260 260
261 261 def magic_run_completer(self, event):
262 262 """Complete files that end in .py or .ipy or .ipynb for the %run command.
263 263 """
264 264 comps = arg_split(event.line, strict=False)
265 265 # relpath should be the current token that we need to complete.
266 266 if (len(comps) > 1) and (not event.line.endswith(' ')):
267 267 relpath = comps[-1].strip("'\"")
268 268 else:
269 269 relpath = ''
270 270
271 271 #print("\nev=", event) # dbg
272 272 #print("rp=", relpath) # dbg
273 273 #print('comps=', comps) # dbg
274 274
275 275 lglob = glob.glob
276 276 isdir = os.path.isdir
277 277 relpath, tilde_expand, tilde_val = expand_user(relpath)
278 278
279 279 # Find if the user has already typed the first filename, after which we
280 280 # should complete on all files, since after the first one other files may
281 281 # be arguments to the input script.
282 282
283 283 if any(magic_run_re.match(c) for c in comps):
284 284 matches = [f.replace('\\','/') + ('/' if isdir(f) else '')
285 285 for f in lglob(relpath+'*')]
286 286 else:
287 287 dirs = [f.replace('\\','/') + "/" for f in lglob(relpath+'*') if isdir(f)]
288 288 pys = [f.replace('\\','/')
289 289 for f in lglob(relpath+'*.py') + lglob(relpath+'*.ipy') +
290 290 lglob(relpath+'*.ipynb') + lglob(relpath + '*.pyw')]
291 291
292 292 matches = dirs + pys
293 293
294 294 #print('run comp:', dirs+pys) # dbg
295 295 return [compress_user(p, tilde_expand, tilde_val) for p in matches]
296 296
297 297
298 298 def cd_completer(self, event):
299 299 """Completer function for cd, which only returns directories."""
300 300 ip = get_ipython()
301 301 relpath = event.symbol
302 302
303 303 #print(event) # dbg
304 304 if event.line.endswith('-b') or ' -b ' in event.line:
305 305 # return only bookmark completions
306 306 bkms = self.db.get('bookmarks', None)
307 307 if bkms:
308 308 return bkms.keys()
309 309 else:
310 310 return []
311 311
312 312 if event.symbol == '-':
313 313 width_dh = str(len(str(len(ip.user_ns['_dh']) + 1)))
314 314 # jump in directory history by number
315 315 fmt = '-%0' + width_dh +'d [%s]'
316 316 ents = [ fmt % (i,s) for i,s in enumerate(ip.user_ns['_dh'])]
317 317 if len(ents) > 1:
318 318 return ents
319 319 return []
320 320
321 321 if event.symbol.startswith('--'):
322 322 return ["--" + os.path.basename(d) for d in ip.user_ns['_dh']]
323 323
324 324 # Expand ~ in path and normalize directory separators.
325 325 relpath, tilde_expand, tilde_val = expand_user(relpath)
326 326 relpath = relpath.replace('\\','/')
327 327
328 328 found = []
329 329 for d in [f.replace('\\','/') + '/' for f in glob.glob(relpath+'*')
330 330 if os.path.isdir(f)]:
331 331 if ' ' in d:
332 332 # we don't want to deal with any of that, complex code
333 333 # for this is elsewhere
334 334 raise TryNext
335 335
336 336 found.append(d)
337 337
338 338 if not found:
339 339 if os.path.isdir(relpath):
340 340 return [compress_user(relpath, tilde_expand, tilde_val)]
341 341
342 342 # if no completions so far, try bookmarks
343 343 bks = self.db.get('bookmarks',{})
344 344 bkmatches = [s for s in bks if s.startswith(event.symbol)]
345 345 if bkmatches:
346 346 return bkmatches
347 347
348 348 raise TryNext
349 349
350 350 return [compress_user(p, tilde_expand, tilde_val) for p in found]
351 351
352 352 def reset_completer(self, event):
353 353 "A completer for %reset magic"
354 354 return '-f -s in out array dhist'.split()
@@ -1,161 +1,178 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 from IPython.core.completerlib import magic_run_completer, module_completion
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 alterntives
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 alterntive
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 alterntives
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 alterntive
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 160 finally:
161 161 sys.path.remove(testsdir)
162
163
164 def test_module_without_init():
165 """
166 Test module without __init__.py.
167
168 https://github.com/ipython/ipython/issues/11226
169 """
170 fake_module_name = "foo"
171 with TemporaryDirectory() as tmpdir:
172 sys.path.insert(0, tmpdir)
173 try:
174 os.makedirs(os.path.join(tmpdir, fake_module_name))
175 s = try_import(mod=fake_module_name)
176 assert s == []
177 finally:
178 sys.path.remove(tmpdir)
General Comments 0
You need to be logged in to leave comments. Login now