##// END OF EJS Templates
Merge pull request #10029 from ivanov/doctest-utils-text...
Thomas Kluyver -
r22982:67870aef merge
parent child Browse files
Show More
@@ -1,431 +1,431 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Suite Runner.
2 """IPython Test Suite Runner.
3
3
4 This module provides a main entry point to a user script to test IPython
4 This module provides a main entry point to a user script to test IPython
5 itself from the command line. There are two ways of running this script:
5 itself from the command line. There are two ways of running this script:
6
6
7 1. With the syntax `iptest all`. This runs our entire test suite by
7 1. With the syntax `iptest all`. This runs our entire test suite by
8 calling this script (with different arguments) recursively. This
8 calling this script (with different arguments) recursively. This
9 causes modules and package to be tested in different processes, using nose
9 causes modules and package to be tested in different processes, using nose
10 or trial where appropriate.
10 or trial where appropriate.
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 the script simply calls nose, but with special command line flags and
12 the script simply calls nose, but with special command line flags and
13 plugins loaded.
13 plugins loaded.
14
14
15 """
15 """
16
16
17 # Copyright (c) IPython Development Team.
17 # Copyright (c) IPython Development Team.
18 # Distributed under the terms of the Modified BSD License.
18 # Distributed under the terms of the Modified BSD License.
19
19
20
20
21 import glob
21 import glob
22 from io import BytesIO
22 from io import BytesIO
23 import os
23 import os
24 import os.path as path
24 import os.path as path
25 import sys
25 import sys
26 from threading import Thread, Lock, Event
26 from threading import Thread, Lock, Event
27 import warnings
27 import warnings
28
28
29 import nose.plugins.builtin
29 import nose.plugins.builtin
30 from nose.plugins.xunit import Xunit
30 from nose.plugins.xunit import Xunit
31 from nose import SkipTest
31 from nose import SkipTest
32 from nose.core import TestProgram
32 from nose.core import TestProgram
33 from nose.plugins import Plugin
33 from nose.plugins import Plugin
34 from nose.util import safe_str
34 from nose.util import safe_str
35
35
36 from IPython import version_info
36 from IPython import version_info
37 from IPython.utils.py3compat import bytes_to_str
37 from IPython.utils.py3compat import bytes_to_str
38 from IPython.utils.importstring import import_item
38 from IPython.utils.importstring import import_item
39 from IPython.testing.plugin.ipdoctest import IPythonDoctest
39 from IPython.testing.plugin.ipdoctest import IPythonDoctest
40 from IPython.external.decorators import KnownFailure, knownfailureif
40 from IPython.external.decorators import KnownFailure, knownfailureif
41
41
42 pjoin = path.join
42 pjoin = path.join
43
43
44
44
45 # Enable printing all warnings raise by IPython's modules
45 # Enable printing all warnings raise by IPython's modules
46 warnings.filterwarnings('ignore', message='.*Matplotlib is building the font cache.*', category=UserWarning, module='.*')
46 warnings.filterwarnings('ignore', message='.*Matplotlib is building the font cache.*', category=UserWarning, module='.*')
47 warnings.filterwarnings('error', message='.*', category=ResourceWarning, module='.*')
47 warnings.filterwarnings('error', message='.*', category=ResourceWarning, module='.*')
48 warnings.filterwarnings('error', message=".*{'config': True}.*", category=DeprecationWarning, module='IPy.*')
48 warnings.filterwarnings('error', message=".*{'config': True}.*", category=DeprecationWarning, module='IPy.*')
49 warnings.filterwarnings('default', message='.*', category=Warning, module='IPy.*')
49 warnings.filterwarnings('default', message='.*', category=Warning, module='IPy.*')
50
50
51 if version_info < (6,):
51 if version_info < (6,):
52 # nose.tools renames all things from `camelCase` to `snake_case` which raise an
52 # nose.tools renames all things from `camelCase` to `snake_case` which raise an
53 # warning with the runner they also import from standard import library. (as of Dec 2015)
53 # warning with the runner they also import from standard import library. (as of Dec 2015)
54 # Ignore, let's revisit that in a couple of years for IPython 6.
54 # Ignore, let's revisit that in a couple of years for IPython 6.
55 warnings.filterwarnings('ignore', message='.*Please use assertEqual instead', category=Warning, module='IPython.*')
55 warnings.filterwarnings('ignore', message='.*Please use assertEqual instead', category=Warning, module='IPython.*')
56
56
57
57
58 # ------------------------------------------------------------------------------
58 # ------------------------------------------------------------------------------
59 # Monkeypatch Xunit to count known failures as skipped.
59 # Monkeypatch Xunit to count known failures as skipped.
60 # ------------------------------------------------------------------------------
60 # ------------------------------------------------------------------------------
61 def monkeypatch_xunit():
61 def monkeypatch_xunit():
62 try:
62 try:
63 knownfailureif(True)(lambda: None)()
63 knownfailureif(True)(lambda: None)()
64 except Exception as e:
64 except Exception as e:
65 KnownFailureTest = type(e)
65 KnownFailureTest = type(e)
66
66
67 def addError(self, test, err, capt=None):
67 def addError(self, test, err, capt=None):
68 if issubclass(err[0], KnownFailureTest):
68 if issubclass(err[0], KnownFailureTest):
69 err = (SkipTest,) + err[1:]
69 err = (SkipTest,) + err[1:]
70 return self.orig_addError(test, err, capt)
70 return self.orig_addError(test, err, capt)
71
71
72 Xunit.orig_addError = Xunit.addError
72 Xunit.orig_addError = Xunit.addError
73 Xunit.addError = addError
73 Xunit.addError = addError
74
74
75 #-----------------------------------------------------------------------------
75 #-----------------------------------------------------------------------------
76 # Check which dependencies are installed and greater than minimum version.
76 # Check which dependencies are installed and greater than minimum version.
77 #-----------------------------------------------------------------------------
77 #-----------------------------------------------------------------------------
78 def extract_version(mod):
78 def extract_version(mod):
79 return mod.__version__
79 return mod.__version__
80
80
81 def test_for(item, min_version=None, callback=extract_version):
81 def test_for(item, min_version=None, callback=extract_version):
82 """Test to see if item is importable, and optionally check against a minimum
82 """Test to see if item is importable, and optionally check against a minimum
83 version.
83 version.
84
84
85 If min_version is given, the default behavior is to check against the
85 If min_version is given, the default behavior is to check against the
86 `__version__` attribute of the item, but specifying `callback` allows you to
86 `__version__` attribute of the item, but specifying `callback` allows you to
87 extract the value you are interested in. e.g::
87 extract the value you are interested in. e.g::
88
88
89 In [1]: import sys
89 In [1]: import sys
90
90
91 In [2]: from IPython.testing.iptest import test_for
91 In [2]: from IPython.testing.iptest import test_for
92
92
93 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
93 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
94 Out[3]: True
94 Out[3]: True
95
95
96 """
96 """
97 try:
97 try:
98 check = import_item(item)
98 check = import_item(item)
99 except (ImportError, RuntimeError):
99 except (ImportError, RuntimeError):
100 # GTK reports Runtime error if it can't be initialized even if it's
100 # GTK reports Runtime error if it can't be initialized even if it's
101 # importable.
101 # importable.
102 return False
102 return False
103 else:
103 else:
104 if min_version:
104 if min_version:
105 if callback:
105 if callback:
106 # extra processing step to get version to compare
106 # extra processing step to get version to compare
107 check = callback(check)
107 check = callback(check)
108
108
109 return check >= min_version
109 return check >= min_version
110 else:
110 else:
111 return True
111 return True
112
112
113 # Global dict where we can store information on what we have and what we don't
113 # Global dict where we can store information on what we have and what we don't
114 # have available at test run time
114 # have available at test run time
115 have = {'matplotlib': test_for('matplotlib'),
115 have = {'matplotlib': test_for('matplotlib'),
116 'pygments': test_for('pygments'),
116 'pygments': test_for('pygments'),
117 'sqlite3': test_for('sqlite3')}
117 'sqlite3': test_for('sqlite3')}
118
118
119 #-----------------------------------------------------------------------------
119 #-----------------------------------------------------------------------------
120 # Test suite definitions
120 # Test suite definitions
121 #-----------------------------------------------------------------------------
121 #-----------------------------------------------------------------------------
122
122
123 test_group_names = ['core',
123 test_group_names = ['core',
124 'extensions', 'lib', 'terminal', 'testing', 'utils',
124 'extensions', 'lib', 'terminal', 'testing', 'utils',
125 ]
125 ]
126
126
127 class TestSection(object):
127 class TestSection(object):
128 def __init__(self, name, includes):
128 def __init__(self, name, includes):
129 self.name = name
129 self.name = name
130 self.includes = includes
130 self.includes = includes
131 self.excludes = []
131 self.excludes = []
132 self.dependencies = []
132 self.dependencies = []
133 self.enabled = True
133 self.enabled = True
134
134
135 def exclude(self, module):
135 def exclude(self, module):
136 if not module.startswith('IPython'):
136 if not module.startswith('IPython'):
137 module = self.includes[0] + "." + module
137 module = self.includes[0] + "." + module
138 self.excludes.append(module.replace('.', os.sep))
138 self.excludes.append(module.replace('.', os.sep))
139
139
140 def requires(self, *packages):
140 def requires(self, *packages):
141 self.dependencies.extend(packages)
141 self.dependencies.extend(packages)
142
142
143 @property
143 @property
144 def will_run(self):
144 def will_run(self):
145 return self.enabled and all(have[p] for p in self.dependencies)
145 return self.enabled and all(have[p] for p in self.dependencies)
146
146
147 # Name -> (include, exclude, dependencies_met)
147 # Name -> (include, exclude, dependencies_met)
148 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
148 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
149
149
150
150
151 # Exclusions and dependencies
151 # Exclusions and dependencies
152 # ---------------------------
152 # ---------------------------
153
153
154 # core:
154 # core:
155 sec = test_sections['core']
155 sec = test_sections['core']
156 if not have['sqlite3']:
156 if not have['sqlite3']:
157 sec.exclude('tests.test_history')
157 sec.exclude('tests.test_history')
158 sec.exclude('history')
158 sec.exclude('history')
159 if not have['matplotlib']:
159 if not have['matplotlib']:
160 sec.exclude('pylabtools'),
160 sec.exclude('pylabtools'),
161 sec.exclude('tests.test_pylabtools')
161 sec.exclude('tests.test_pylabtools')
162
162
163 # lib:
163 # lib:
164 sec = test_sections['lib']
164 sec = test_sections['lib']
165 sec.exclude('kernel')
165 sec.exclude('kernel')
166 if not have['pygments']:
166 if not have['pygments']:
167 sec.exclude('tests.test_lexers')
167 sec.exclude('tests.test_lexers')
168 # We do this unconditionally, so that the test suite doesn't import
168 # We do this unconditionally, so that the test suite doesn't import
169 # gtk, changing the default encoding and masking some unicode bugs.
169 # gtk, changing the default encoding and masking some unicode bugs.
170 sec.exclude('inputhookgtk')
170 sec.exclude('inputhookgtk')
171 # We also do this unconditionally, because wx can interfere with Unix signals.
171 # We also do this unconditionally, because wx can interfere with Unix signals.
172 # There are currently no tests for it anyway.
172 # There are currently no tests for it anyway.
173 sec.exclude('inputhookwx')
173 sec.exclude('inputhookwx')
174 # Testing inputhook will need a lot of thought, to figure out
174 # Testing inputhook will need a lot of thought, to figure out
175 # how to have tests that don't lock up with the gui event
175 # how to have tests that don't lock up with the gui event
176 # loops in the picture
176 # loops in the picture
177 sec.exclude('inputhook')
177 sec.exclude('inputhook')
178
178
179 # testing:
179 # testing:
180 sec = test_sections['testing']
180 sec = test_sections['testing']
181 # These have to be skipped on win32 because they use echo, rm, cd, etc.
181 # These have to be skipped on win32 because they use echo, rm, cd, etc.
182 # See ticket https://github.com/ipython/ipython/issues/87
182 # See ticket https://github.com/ipython/ipython/issues/87
183 if sys.platform == 'win32':
183 if sys.platform == 'win32':
184 sec.exclude('plugin.test_exampleip')
184 sec.exclude('plugin.test_exampleip')
185 sec.exclude('plugin.dtexample')
185 sec.exclude('plugin.dtexample')
186
186
187 # don't run jupyter_console tests found via shim
187 # don't run jupyter_console tests found via shim
188 test_sections['terminal'].exclude('console')
188 test_sections['terminal'].exclude('console')
189
189
190 # extensions:
190 # extensions:
191 sec = test_sections['extensions']
191 sec = test_sections['extensions']
192 # This is deprecated in favour of rpy2
192 # This is deprecated in favour of rpy2
193 sec.exclude('rmagic')
193 sec.exclude('rmagic')
194 # autoreload does some strange stuff, so move it to its own test section
194 # autoreload does some strange stuff, so move it to its own test section
195 sec.exclude('autoreload')
195 sec.exclude('autoreload')
196 sec.exclude('tests.test_autoreload')
196 sec.exclude('tests.test_autoreload')
197 test_sections['autoreload'] = TestSection('autoreload',
197 test_sections['autoreload'] = TestSection('autoreload',
198 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
198 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
199 test_group_names.append('autoreload')
199 test_group_names.append('autoreload')
200
200
201
201
202 #-----------------------------------------------------------------------------
202 #-----------------------------------------------------------------------------
203 # Functions and classes
203 # Functions and classes
204 #-----------------------------------------------------------------------------
204 #-----------------------------------------------------------------------------
205
205
206 def check_exclusions_exist():
206 def check_exclusions_exist():
207 from IPython.paths import get_ipython_package_dir
207 from IPython.paths import get_ipython_package_dir
208 from warnings import warn
208 from warnings import warn
209 parent = os.path.dirname(get_ipython_package_dir())
209 parent = os.path.dirname(get_ipython_package_dir())
210 for sec in test_sections:
210 for sec in test_sections:
211 for pattern in sec.exclusions:
211 for pattern in sec.exclusions:
212 fullpath = pjoin(parent, pattern)
212 fullpath = pjoin(parent, pattern)
213 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
213 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
214 warn("Excluding nonexistent file: %r" % pattern)
214 warn("Excluding nonexistent file: %r" % pattern)
215
215
216
216
217 class ExclusionPlugin(Plugin):
217 class ExclusionPlugin(Plugin):
218 """A nose plugin to effect our exclusions of files and directories.
218 """A nose plugin to effect our exclusions of files and directories.
219 """
219 """
220 name = 'exclusions'
220 name = 'exclusions'
221 score = 3000 # Should come before any other plugins
221 score = 3000 # Should come before any other plugins
222
222
223 def __init__(self, exclude_patterns=None):
223 def __init__(self, exclude_patterns=None):
224 """
224 """
225 Parameters
225 Parameters
226 ----------
226 ----------
227
227
228 exclude_patterns : sequence of strings, optional
228 exclude_patterns : sequence of strings, optional
229 Filenames containing these patterns (as raw strings, not as regular
229 Filenames containing these patterns (as raw strings, not as regular
230 expressions) are excluded from the tests.
230 expressions) are excluded from the tests.
231 """
231 """
232 self.exclude_patterns = exclude_patterns or []
232 self.exclude_patterns = exclude_patterns or []
233 super(ExclusionPlugin, self).__init__()
233 super(ExclusionPlugin, self).__init__()
234
234
235 def options(self, parser, env=os.environ):
235 def options(self, parser, env=os.environ):
236 Plugin.options(self, parser, env)
236 Plugin.options(self, parser, env)
237
237
238 def configure(self, options, config):
238 def configure(self, options, config):
239 Plugin.configure(self, options, config)
239 Plugin.configure(self, options, config)
240 # Override nose trying to disable plugin.
240 # Override nose trying to disable plugin.
241 self.enabled = True
241 self.enabled = True
242
242
243 def wantFile(self, filename):
243 def wantFile(self, filename):
244 """Return whether the given filename should be scanned for tests.
244 """Return whether the given filename should be scanned for tests.
245 """
245 """
246 if any(pat in filename for pat in self.exclude_patterns):
246 if any(pat in filename for pat in self.exclude_patterns):
247 return False
247 return False
248 return None
248 return None
249
249
250 def wantDirectory(self, directory):
250 def wantDirectory(self, directory):
251 """Return whether the given directory should be scanned for tests.
251 """Return whether the given directory should be scanned for tests.
252 """
252 """
253 if any(pat in directory for pat in self.exclude_patterns):
253 if any(pat in directory for pat in self.exclude_patterns):
254 return False
254 return False
255 return None
255 return None
256
256
257
257
258 class StreamCapturer(Thread):
258 class StreamCapturer(Thread):
259 daemon = True # Don't hang if main thread crashes
259 daemon = True # Don't hang if main thread crashes
260 started = False
260 started = False
261 def __init__(self, echo=False):
261 def __init__(self, echo=False):
262 super(StreamCapturer, self).__init__()
262 super(StreamCapturer, self).__init__()
263 self.echo = echo
263 self.echo = echo
264 self.streams = []
264 self.streams = []
265 self.buffer = BytesIO()
265 self.buffer = BytesIO()
266 self.readfd, self.writefd = os.pipe()
266 self.readfd, self.writefd = os.pipe()
267 self.buffer_lock = Lock()
267 self.buffer_lock = Lock()
268 self.stop = Event()
268 self.stop = Event()
269
269
270 def run(self):
270 def run(self):
271 self.started = True
271 self.started = True
272
272
273 while not self.stop.is_set():
273 while not self.stop.is_set():
274 chunk = os.read(self.readfd, 1024)
274 chunk = os.read(self.readfd, 1024)
275
275
276 with self.buffer_lock:
276 with self.buffer_lock:
277 self.buffer.write(chunk)
277 self.buffer.write(chunk)
278 if self.echo:
278 if self.echo:
279 sys.stdout.write(bytes_to_str(chunk))
279 sys.stdout.write(bytes_to_str(chunk))
280
280
281 os.close(self.readfd)
281 os.close(self.readfd)
282 os.close(self.writefd)
282 os.close(self.writefd)
283
283
284 def reset_buffer(self):
284 def reset_buffer(self):
285 with self.buffer_lock:
285 with self.buffer_lock:
286 self.buffer.truncate(0)
286 self.buffer.truncate(0)
287 self.buffer.seek(0)
287 self.buffer.seek(0)
288
288
289 def get_buffer(self):
289 def get_buffer(self):
290 with self.buffer_lock:
290 with self.buffer_lock:
291 return self.buffer.getvalue()
291 return self.buffer.getvalue()
292
292
293 def ensure_started(self):
293 def ensure_started(self):
294 if not self.started:
294 if not self.started:
295 self.start()
295 self.start()
296
296
297 def halt(self):
297 def halt(self):
298 """Safely stop the thread."""
298 """Safely stop the thread."""
299 if not self.started:
299 if not self.started:
300 return
300 return
301
301
302 self.stop.set()
302 self.stop.set()
303 os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
303 os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
304 self.join()
304 self.join()
305
305
306 class SubprocessStreamCapturePlugin(Plugin):
306 class SubprocessStreamCapturePlugin(Plugin):
307 name='subprocstreams'
307 name='subprocstreams'
308 def __init__(self):
308 def __init__(self):
309 Plugin.__init__(self)
309 Plugin.__init__(self)
310 self.stream_capturer = StreamCapturer()
310 self.stream_capturer = StreamCapturer()
311 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
311 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
312 # This is ugly, but distant parts of the test machinery need to be able
312 # This is ugly, but distant parts of the test machinery need to be able
313 # to redirect streams, so we make the object globally accessible.
313 # to redirect streams, so we make the object globally accessible.
314 nose.iptest_stdstreams_fileno = self.get_write_fileno
314 nose.iptest_stdstreams_fileno = self.get_write_fileno
315
315
316 def get_write_fileno(self):
316 def get_write_fileno(self):
317 if self.destination == 'capture':
317 if self.destination == 'capture':
318 self.stream_capturer.ensure_started()
318 self.stream_capturer.ensure_started()
319 return self.stream_capturer.writefd
319 return self.stream_capturer.writefd
320 elif self.destination == 'discard':
320 elif self.destination == 'discard':
321 return os.open(os.devnull, os.O_WRONLY)
321 return os.open(os.devnull, os.O_WRONLY)
322 else:
322 else:
323 return sys.__stdout__.fileno()
323 return sys.__stdout__.fileno()
324
324
325 def configure(self, options, config):
325 def configure(self, options, config):
326 Plugin.configure(self, options, config)
326 Plugin.configure(self, options, config)
327 # Override nose trying to disable plugin.
327 # Override nose trying to disable plugin.
328 if self.destination == 'capture':
328 if self.destination == 'capture':
329 self.enabled = True
329 self.enabled = True
330
330
331 def startTest(self, test):
331 def startTest(self, test):
332 # Reset log capture
332 # Reset log capture
333 self.stream_capturer.reset_buffer()
333 self.stream_capturer.reset_buffer()
334
334
335 def formatFailure(self, test, err):
335 def formatFailure(self, test, err):
336 # Show output
336 # Show output
337 ec, ev, tb = err
337 ec, ev, tb = err
338 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
338 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
339 if captured.strip():
339 if captured.strip():
340 ev = safe_str(ev)
340 ev = safe_str(ev)
341 out = [ev, '>> begin captured subprocess output <<',
341 out = [ev, '>> begin captured subprocess output <<',
342 captured,
342 captured,
343 '>> end captured subprocess output <<']
343 '>> end captured subprocess output <<']
344 return ec, '\n'.join(out), tb
344 return ec, '\n'.join(out), tb
345
345
346 return err
346 return err
347
347
348 formatError = formatFailure
348 formatError = formatFailure
349
349
350 def finalize(self, result):
350 def finalize(self, result):
351 self.stream_capturer.halt()
351 self.stream_capturer.halt()
352
352
353
353
354 def run_iptest():
354 def run_iptest():
355 """Run the IPython test suite using nose.
355 """Run the IPython test suite using nose.
356
356
357 This function is called when this script is **not** called with the form
357 This function is called when this script is **not** called with the form
358 `iptest all`. It simply calls nose with appropriate command line flags
358 `iptest all`. It simply calls nose with appropriate command line flags
359 and accepts all of the standard nose arguments.
359 and accepts all of the standard nose arguments.
360 """
360 """
361 # Apply our monkeypatch to Xunit
361 # Apply our monkeypatch to Xunit
362 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
362 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
363 monkeypatch_xunit()
363 monkeypatch_xunit()
364
364
365 arg1 = sys.argv[1]
365 arg1 = sys.argv[1]
366 if arg1 in test_sections:
366 if arg1 in test_sections:
367 section = test_sections[arg1]
367 section = test_sections[arg1]
368 sys.argv[1:2] = section.includes
368 sys.argv[1:2] = section.includes
369 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
369 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
370 section = test_sections[arg1[8:]]
370 section = test_sections[arg1[8:]]
371 sys.argv[1:2] = section.includes
371 sys.argv[1:2] = section.includes
372 else:
372 else:
373 section = TestSection(arg1, includes=[arg1])
373 section = TestSection(arg1, includes=[arg1])
374
374
375
375
376 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
376 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
377 # We add --exe because of setuptools' imbecility (it
377 # We add --exe because of setuptools' imbecility (it
378 # blindly does chmod +x on ALL files). Nose does the
378 # blindly does chmod +x on ALL files). Nose does the
379 # right thing and it tries to avoid executables,
379 # right thing and it tries to avoid executables,
380 # setuptools unfortunately forces our hand here. This
380 # setuptools unfortunately forces our hand here. This
381 # has been discussed on the distutils list and the
381 # has been discussed on the distutils list and the
382 # setuptools devs refuse to fix this problem!
382 # setuptools devs refuse to fix this problem!
383 '--exe',
383 '--exe',
384 ]
384 ]
385 if '-a' not in argv and '-A' not in argv:
385 if '-a' not in argv and '-A' not in argv:
386 argv = argv + ['-a', '!crash']
386 argv = argv + ['-a', '!crash']
387
387
388 if nose.__version__ >= '0.11':
388 if nose.__version__ >= '0.11':
389 # I don't fully understand why we need this one, but depending on what
389 # I don't fully understand why we need this one, but depending on what
390 # directory the test suite is run from, if we don't give it, 0 tests
390 # directory the test suite is run from, if we don't give it, 0 tests
391 # get run. Specifically, if the test suite is run from the source dir
391 # get run. Specifically, if the test suite is run from the source dir
392 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
392 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
393 # even if the same call done in this directory works fine). It appears
393 # even if the same call done in this directory works fine). It appears
394 # that if the requested package is in the current dir, nose bails early
394 # that if the requested package is in the current dir, nose bails early
395 # by default. Since it's otherwise harmless, leave it in by default
395 # by default. Since it's otherwise harmless, leave it in by default
396 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
396 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
397 argv.append('--traverse-namespace')
397 argv.append('--traverse-namespace')
398
398
399 plugins = [ ExclusionPlugin(section.excludes), KnownFailure(),
399 plugins = [ ExclusionPlugin(section.excludes), KnownFailure(),
400 SubprocessStreamCapturePlugin() ]
400 SubprocessStreamCapturePlugin() ]
401
401
402 # we still have some vestigial doctests in core
402 # we still have some vestigial doctests in core
403 if (section.name.startswith(('core', 'IPython.core'))):
403 if (section.name.startswith(('core', 'IPython.core', 'IPython.utils'))):
404 plugins.append(IPythonDoctest())
404 plugins.append(IPythonDoctest())
405 argv.extend([
405 argv.extend([
406 '--with-ipdoctest',
406 '--with-ipdoctest',
407 '--ipdoctest-tests',
407 '--ipdoctest-tests',
408 '--ipdoctest-extension=txt',
408 '--ipdoctest-extension=txt',
409 ])
409 ])
410
410
411
411
412 # Use working directory set by parent process (see iptestcontroller)
412 # Use working directory set by parent process (see iptestcontroller)
413 if 'IPTEST_WORKING_DIR' in os.environ:
413 if 'IPTEST_WORKING_DIR' in os.environ:
414 os.chdir(os.environ['IPTEST_WORKING_DIR'])
414 os.chdir(os.environ['IPTEST_WORKING_DIR'])
415
415
416 # We need a global ipython running in this process, but the special
416 # We need a global ipython running in this process, but the special
417 # in-process group spawns its own IPython kernels, so for *that* group we
417 # in-process group spawns its own IPython kernels, so for *that* group we
418 # must avoid also opening the global one (otherwise there's a conflict of
418 # must avoid also opening the global one (otherwise there's a conflict of
419 # singletons). Ultimately the solution to this problem is to refactor our
419 # singletons). Ultimately the solution to this problem is to refactor our
420 # assumptions about what needs to be a singleton and what doesn't (app
420 # assumptions about what needs to be a singleton and what doesn't (app
421 # objects should, individual shells shouldn't). But for now, this
421 # objects should, individual shells shouldn't). But for now, this
422 # workaround allows the test suite for the inprocess module to complete.
422 # workaround allows the test suite for the inprocess module to complete.
423 if 'kernel.inprocess' not in section.name:
423 if 'kernel.inprocess' not in section.name:
424 from IPython.testing import globalipapp
424 from IPython.testing import globalipapp
425 globalipapp.start_ipython()
425 globalipapp.start_ipython()
426
426
427 # Now nose can run
427 # Now nose can run
428 TestProgram(argv=argv, addplugins=plugins)
428 TestProgram(argv=argv, addplugins=plugins)
429
429
430 if __name__ == '__main__':
430 if __name__ == '__main__':
431 run_iptest()
431 run_iptest()
@@ -1,779 +1,774 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 Utilities for working with strings and text.
3 Utilities for working with strings and text.
4
4
5 Inheritance diagram:
5 Inheritance diagram:
6
6
7 .. inheritance-diagram:: IPython.utils.text
7 .. inheritance-diagram:: IPython.utils.text
8 :parts: 3
8 :parts: 3
9 """
9 """
10
10
11 import os
11 import os
12 import re
12 import re
13 import sys
13 import sys
14 import textwrap
14 import textwrap
15 from string import Formatter
15 from string import Formatter
16 try:
16 try:
17 from pathlib import Path
17 from pathlib import Path
18 except ImportError:
18 except ImportError:
19 # Python 2 backport
19 # Python 2 backport
20 from pathlib2 import Path
20 from pathlib2 import Path
21
21
22 from IPython.utils import py3compat
22 from IPython.utils import py3compat
23
23
24 # datetime.strftime date format for ipython
24 # datetime.strftime date format for ipython
25 if sys.platform == 'win32':
25 if sys.platform == 'win32':
26 date_format = "%B %d, %Y"
26 date_format = "%B %d, %Y"
27 else:
27 else:
28 date_format = "%B %-d, %Y"
28 date_format = "%B %-d, %Y"
29
29
30 class LSString(str):
30 class LSString(str):
31 """String derivative with a special access attributes.
31 """String derivative with a special access attributes.
32
32
33 These are normal strings, but with the special attributes:
33 These are normal strings, but with the special attributes:
34
34
35 .l (or .list) : value as list (split on newlines).
35 .l (or .list) : value as list (split on newlines).
36 .n (or .nlstr): original value (the string itself).
36 .n (or .nlstr): original value (the string itself).
37 .s (or .spstr): value as whitespace-separated string.
37 .s (or .spstr): value as whitespace-separated string.
38 .p (or .paths): list of path objects (requires path.py package)
38 .p (or .paths): list of path objects (requires path.py package)
39
39
40 Any values which require transformations are computed only once and
40 Any values which require transformations are computed only once and
41 cached.
41 cached.
42
42
43 Such strings are very useful to efficiently interact with the shell, which
43 Such strings are very useful to efficiently interact with the shell, which
44 typically only understands whitespace-separated options for commands."""
44 typically only understands whitespace-separated options for commands."""
45
45
46 def get_list(self):
46 def get_list(self):
47 try:
47 try:
48 return self.__list
48 return self.__list
49 except AttributeError:
49 except AttributeError:
50 self.__list = self.split('\n')
50 self.__list = self.split('\n')
51 return self.__list
51 return self.__list
52
52
53 l = list = property(get_list)
53 l = list = property(get_list)
54
54
55 def get_spstr(self):
55 def get_spstr(self):
56 try:
56 try:
57 return self.__spstr
57 return self.__spstr
58 except AttributeError:
58 except AttributeError:
59 self.__spstr = self.replace('\n',' ')
59 self.__spstr = self.replace('\n',' ')
60 return self.__spstr
60 return self.__spstr
61
61
62 s = spstr = property(get_spstr)
62 s = spstr = property(get_spstr)
63
63
64 def get_nlstr(self):
64 def get_nlstr(self):
65 return self
65 return self
66
66
67 n = nlstr = property(get_nlstr)
67 n = nlstr = property(get_nlstr)
68
68
69 def get_paths(self):
69 def get_paths(self):
70 try:
70 try:
71 return self.__paths
71 return self.__paths
72 except AttributeError:
72 except AttributeError:
73 self.__paths = [Path(p) for p in self.split('\n') if os.path.exists(p)]
73 self.__paths = [Path(p) for p in self.split('\n') if os.path.exists(p)]
74 return self.__paths
74 return self.__paths
75
75
76 p = paths = property(get_paths)
76 p = paths = property(get_paths)
77
77
78 # FIXME: We need to reimplement type specific displayhook and then add this
78 # FIXME: We need to reimplement type specific displayhook and then add this
79 # back as a custom printer. This should also be moved outside utils into the
79 # back as a custom printer. This should also be moved outside utils into the
80 # core.
80 # core.
81
81
82 # def print_lsstring(arg):
82 # def print_lsstring(arg):
83 # """ Prettier (non-repr-like) and more informative printer for LSString """
83 # """ Prettier (non-repr-like) and more informative printer for LSString """
84 # print "LSString (.p, .n, .l, .s available). Value:"
84 # print "LSString (.p, .n, .l, .s available). Value:"
85 # print arg
85 # print arg
86 #
86 #
87 #
87 #
88 # print_lsstring = result_display.when_type(LSString)(print_lsstring)
88 # print_lsstring = result_display.when_type(LSString)(print_lsstring)
89
89
90
90
91 class SList(list):
91 class SList(list):
92 """List derivative with a special access attributes.
92 """List derivative with a special access attributes.
93
93
94 These are normal lists, but with the special attributes:
94 These are normal lists, but with the special attributes:
95
95
96 * .l (or .list) : value as list (the list itself).
96 * .l (or .list) : value as list (the list itself).
97 * .n (or .nlstr): value as a string, joined on newlines.
97 * .n (or .nlstr): value as a string, joined on newlines.
98 * .s (or .spstr): value as a string, joined on spaces.
98 * .s (or .spstr): value as a string, joined on spaces.
99 * .p (or .paths): list of path objects (requires path.py package)
99 * .p (or .paths): list of path objects (requires path.py package)
100
100
101 Any values which require transformations are computed only once and
101 Any values which require transformations are computed only once and
102 cached."""
102 cached."""
103
103
104 def get_list(self):
104 def get_list(self):
105 return self
105 return self
106
106
107 l = list = property(get_list)
107 l = list = property(get_list)
108
108
109 def get_spstr(self):
109 def get_spstr(self):
110 try:
110 try:
111 return self.__spstr
111 return self.__spstr
112 except AttributeError:
112 except AttributeError:
113 self.__spstr = ' '.join(self)
113 self.__spstr = ' '.join(self)
114 return self.__spstr
114 return self.__spstr
115
115
116 s = spstr = property(get_spstr)
116 s = spstr = property(get_spstr)
117
117
118 def get_nlstr(self):
118 def get_nlstr(self):
119 try:
119 try:
120 return self.__nlstr
120 return self.__nlstr
121 except AttributeError:
121 except AttributeError:
122 self.__nlstr = '\n'.join(self)
122 self.__nlstr = '\n'.join(self)
123 return self.__nlstr
123 return self.__nlstr
124
124
125 n = nlstr = property(get_nlstr)
125 n = nlstr = property(get_nlstr)
126
126
127 def get_paths(self):
127 def get_paths(self):
128 try:
128 try:
129 return self.__paths
129 return self.__paths
130 except AttributeError:
130 except AttributeError:
131 self.__paths = [Path(p) for p in self if os.path.exists(p)]
131 self.__paths = [Path(p) for p in self if os.path.exists(p)]
132 return self.__paths
132 return self.__paths
133
133
134 p = paths = property(get_paths)
134 p = paths = property(get_paths)
135
135
136 def grep(self, pattern, prune = False, field = None):
136 def grep(self, pattern, prune = False, field = None):
137 """ Return all strings matching 'pattern' (a regex or callable)
137 """ Return all strings matching 'pattern' (a regex or callable)
138
138
139 This is case-insensitive. If prune is true, return all items
139 This is case-insensitive. If prune is true, return all items
140 NOT matching the pattern.
140 NOT matching the pattern.
141
141
142 If field is specified, the match must occur in the specified
142 If field is specified, the match must occur in the specified
143 whitespace-separated field.
143 whitespace-separated field.
144
144
145 Examples::
145 Examples::
146
146
147 a.grep( lambda x: x.startswith('C') )
147 a.grep( lambda x: x.startswith('C') )
148 a.grep('Cha.*log', prune=1)
148 a.grep('Cha.*log', prune=1)
149 a.grep('chm', field=-1)
149 a.grep('chm', field=-1)
150 """
150 """
151
151
152 def match_target(s):
152 def match_target(s):
153 if field is None:
153 if field is None:
154 return s
154 return s
155 parts = s.split()
155 parts = s.split()
156 try:
156 try:
157 tgt = parts[field]
157 tgt = parts[field]
158 return tgt
158 return tgt
159 except IndexError:
159 except IndexError:
160 return ""
160 return ""
161
161
162 if isinstance(pattern, py3compat.string_types):
162 if isinstance(pattern, py3compat.string_types):
163 pred = lambda x : re.search(pattern, x, re.IGNORECASE)
163 pred = lambda x : re.search(pattern, x, re.IGNORECASE)
164 else:
164 else:
165 pred = pattern
165 pred = pattern
166 if not prune:
166 if not prune:
167 return SList([el for el in self if pred(match_target(el))])
167 return SList([el for el in self if pred(match_target(el))])
168 else:
168 else:
169 return SList([el for el in self if not pred(match_target(el))])
169 return SList([el for el in self if not pred(match_target(el))])
170
170
171 def fields(self, *fields):
171 def fields(self, *fields):
172 """ Collect whitespace-separated fields from string list
172 """ Collect whitespace-separated fields from string list
173
173
174 Allows quick awk-like usage of string lists.
174 Allows quick awk-like usage of string lists.
175
175
176 Example data (in var a, created by 'a = !ls -l')::
176 Example data (in var a, created by 'a = !ls -l')::
177
177
178 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
178 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
179 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
179 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
180
180
181 * ``a.fields(0)`` is ``['-rwxrwxrwx', 'drwxrwxrwx+']``
181 * ``a.fields(0)`` is ``['-rwxrwxrwx', 'drwxrwxrwx+']``
182 * ``a.fields(1,0)`` is ``['1 -rwxrwxrwx', '6 drwxrwxrwx+']``
182 * ``a.fields(1,0)`` is ``['1 -rwxrwxrwx', '6 drwxrwxrwx+']``
183 (note the joining by space).
183 (note the joining by space).
184 * ``a.fields(-1)`` is ``['ChangeLog', 'IPython']``
184 * ``a.fields(-1)`` is ``['ChangeLog', 'IPython']``
185
185
186 IndexErrors are ignored.
186 IndexErrors are ignored.
187
187
188 Without args, fields() just split()'s the strings.
188 Without args, fields() just split()'s the strings.
189 """
189 """
190 if len(fields) == 0:
190 if len(fields) == 0:
191 return [el.split() for el in self]
191 return [el.split() for el in self]
192
192
193 res = SList()
193 res = SList()
194 for el in [f.split() for f in self]:
194 for el in [f.split() for f in self]:
195 lineparts = []
195 lineparts = []
196
196
197 for fd in fields:
197 for fd in fields:
198 try:
198 try:
199 lineparts.append(el[fd])
199 lineparts.append(el[fd])
200 except IndexError:
200 except IndexError:
201 pass
201 pass
202 if lineparts:
202 if lineparts:
203 res.append(" ".join(lineparts))
203 res.append(" ".join(lineparts))
204
204
205 return res
205 return res
206
206
207 def sort(self,field= None, nums = False):
207 def sort(self,field= None, nums = False):
208 """ sort by specified fields (see fields())
208 """ sort by specified fields (see fields())
209
209
210 Example::
210 Example::
211
211
212 a.sort(1, nums = True)
212 a.sort(1, nums = True)
213
213
214 Sorts a by second field, in numerical order (so that 21 > 3)
214 Sorts a by second field, in numerical order (so that 21 > 3)
215
215
216 """
216 """
217
217
218 #decorate, sort, undecorate
218 #decorate, sort, undecorate
219 if field is not None:
219 if field is not None:
220 dsu = [[SList([line]).fields(field), line] for line in self]
220 dsu = [[SList([line]).fields(field), line] for line in self]
221 else:
221 else:
222 dsu = [[line, line] for line in self]
222 dsu = [[line, line] for line in self]
223 if nums:
223 if nums:
224 for i in range(len(dsu)):
224 for i in range(len(dsu)):
225 numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
225 numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
226 try:
226 try:
227 n = int(numstr)
227 n = int(numstr)
228 except ValueError:
228 except ValueError:
229 n = 0
229 n = 0
230 dsu[i][0] = n
230 dsu[i][0] = n
231
231
232
232
233 dsu.sort()
233 dsu.sort()
234 return SList([t[1] for t in dsu])
234 return SList([t[1] for t in dsu])
235
235
236
236
237 # FIXME: We need to reimplement type specific displayhook and then add this
237 # FIXME: We need to reimplement type specific displayhook and then add this
238 # back as a custom printer. This should also be moved outside utils into the
238 # back as a custom printer. This should also be moved outside utils into the
239 # core.
239 # core.
240
240
241 # def print_slist(arg):
241 # def print_slist(arg):
242 # """ Prettier (non-repr-like) and more informative printer for SList """
242 # """ Prettier (non-repr-like) and more informative printer for SList """
243 # print "SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):"
243 # print "SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):"
244 # if hasattr(arg, 'hideonce') and arg.hideonce:
244 # if hasattr(arg, 'hideonce') and arg.hideonce:
245 # arg.hideonce = False
245 # arg.hideonce = False
246 # return
246 # return
247 #
247 #
248 # nlprint(arg) # This was a nested list printer, now removed.
248 # nlprint(arg) # This was a nested list printer, now removed.
249 #
249 #
250 # print_slist = result_display.when_type(SList)(print_slist)
250 # print_slist = result_display.when_type(SList)(print_slist)
251
251
252
252
253 def indent(instr,nspaces=4, ntabs=0, flatten=False):
253 def indent(instr,nspaces=4, ntabs=0, flatten=False):
254 """Indent a string a given number of spaces or tabstops.
254 """Indent a string a given number of spaces or tabstops.
255
255
256 indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces.
256 indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces.
257
257
258 Parameters
258 Parameters
259 ----------
259 ----------
260
260
261 instr : basestring
261 instr : basestring
262 The string to be indented.
262 The string to be indented.
263 nspaces : int (default: 4)
263 nspaces : int (default: 4)
264 The number of spaces to be indented.
264 The number of spaces to be indented.
265 ntabs : int (default: 0)
265 ntabs : int (default: 0)
266 The number of tabs to be indented.
266 The number of tabs to be indented.
267 flatten : bool (default: False)
267 flatten : bool (default: False)
268 Whether to scrub existing indentation. If True, all lines will be
268 Whether to scrub existing indentation. If True, all lines will be
269 aligned to the same indentation. If False, existing indentation will
269 aligned to the same indentation. If False, existing indentation will
270 be strictly increased.
270 be strictly increased.
271
271
272 Returns
272 Returns
273 -------
273 -------
274
274
275 str|unicode : string indented by ntabs and nspaces.
275 str|unicode : string indented by ntabs and nspaces.
276
276
277 """
277 """
278 if instr is None:
278 if instr is None:
279 return
279 return
280 ind = '\t'*ntabs+' '*nspaces
280 ind = '\t'*ntabs+' '*nspaces
281 if flatten:
281 if flatten:
282 pat = re.compile(r'^\s*', re.MULTILINE)
282 pat = re.compile(r'^\s*', re.MULTILINE)
283 else:
283 else:
284 pat = re.compile(r'^', re.MULTILINE)
284 pat = re.compile(r'^', re.MULTILINE)
285 outstr = re.sub(pat, ind, instr)
285 outstr = re.sub(pat, ind, instr)
286 if outstr.endswith(os.linesep+ind):
286 if outstr.endswith(os.linesep+ind):
287 return outstr[:-len(ind)]
287 return outstr[:-len(ind)]
288 else:
288 else:
289 return outstr
289 return outstr
290
290
291
291
292 def list_strings(arg):
292 def list_strings(arg):
293 """Always return a list of strings, given a string or list of strings
293 """Always return a list of strings, given a string or list of strings
294 as input.
294 as input.
295
295
296 Examples
296 Examples
297 --------
297 --------
298 ::
298 ::
299
299
300 In [7]: list_strings('A single string')
300 In [7]: list_strings('A single string')
301 Out[7]: ['A single string']
301 Out[7]: ['A single string']
302
302
303 In [8]: list_strings(['A single string in a list'])
303 In [8]: list_strings(['A single string in a list'])
304 Out[8]: ['A single string in a list']
304 Out[8]: ['A single string in a list']
305
305
306 In [9]: list_strings(['A','list','of','strings'])
306 In [9]: list_strings(['A','list','of','strings'])
307 Out[9]: ['A', 'list', 'of', 'strings']
307 Out[9]: ['A', 'list', 'of', 'strings']
308 """
308 """
309
309
310 if isinstance(arg, py3compat.string_types): return [arg]
310 if isinstance(arg, py3compat.string_types): return [arg]
311 else: return arg
311 else: return arg
312
312
313
313
314 def marquee(txt='',width=78,mark='*'):
314 def marquee(txt='',width=78,mark='*'):
315 """Return the input string centered in a 'marquee'.
315 """Return the input string centered in a 'marquee'.
316
316
317 Examples
317 Examples
318 --------
318 --------
319 ::
319 ::
320
320
321 In [16]: marquee('A test',40)
321 In [16]: marquee('A test',40)
322 Out[16]: '**************** A test ****************'
322 Out[16]: '**************** A test ****************'
323
323
324 In [17]: marquee('A test',40,'-')
324 In [17]: marquee('A test',40,'-')
325 Out[17]: '---------------- A test ----------------'
325 Out[17]: '---------------- A test ----------------'
326
326
327 In [18]: marquee('A test',40,' ')
327 In [18]: marquee('A test',40,' ')
328 Out[18]: ' A test '
328 Out[18]: ' A test '
329
329
330 """
330 """
331 if not txt:
331 if not txt:
332 return (mark*width)[:width]
332 return (mark*width)[:width]
333 nmark = (width-len(txt)-2)//len(mark)//2
333 nmark = (width-len(txt)-2)//len(mark)//2
334 if nmark < 0: nmark =0
334 if nmark < 0: nmark =0
335 marks = mark*nmark
335 marks = mark*nmark
336 return '%s %s %s' % (marks,txt,marks)
336 return '%s %s %s' % (marks,txt,marks)
337
337
338
338
339 ini_spaces_re = re.compile(r'^(\s+)')
339 ini_spaces_re = re.compile(r'^(\s+)')
340
340
341 def num_ini_spaces(strng):
341 def num_ini_spaces(strng):
342 """Return the number of initial spaces in a string"""
342 """Return the number of initial spaces in a string"""
343
343
344 ini_spaces = ini_spaces_re.match(strng)
344 ini_spaces = ini_spaces_re.match(strng)
345 if ini_spaces:
345 if ini_spaces:
346 return ini_spaces.end()
346 return ini_spaces.end()
347 else:
347 else:
348 return 0
348 return 0
349
349
350
350
351 def format_screen(strng):
351 def format_screen(strng):
352 """Format a string for screen printing.
352 """Format a string for screen printing.
353
353
354 This removes some latex-type format codes."""
354 This removes some latex-type format codes."""
355 # Paragraph continue
355 # Paragraph continue
356 par_re = re.compile(r'\\$',re.MULTILINE)
356 par_re = re.compile(r'\\$',re.MULTILINE)
357 strng = par_re.sub('',strng)
357 strng = par_re.sub('',strng)
358 return strng
358 return strng
359
359
360
360
361 def dedent(text):
361 def dedent(text):
362 """Equivalent of textwrap.dedent that ignores unindented first line.
362 """Equivalent of textwrap.dedent that ignores unindented first line.
363
363
364 This means it will still dedent strings like:
364 This means it will still dedent strings like:
365 '''foo
365 '''foo
366 is a bar
366 is a bar
367 '''
367 '''
368
368
369 For use in wrap_paragraphs.
369 For use in wrap_paragraphs.
370 """
370 """
371
371
372 if text.startswith('\n'):
372 if text.startswith('\n'):
373 # text starts with blank line, don't ignore the first line
373 # text starts with blank line, don't ignore the first line
374 return textwrap.dedent(text)
374 return textwrap.dedent(text)
375
375
376 # split first line
376 # split first line
377 splits = text.split('\n',1)
377 splits = text.split('\n',1)
378 if len(splits) == 1:
378 if len(splits) == 1:
379 # only one line
379 # only one line
380 return textwrap.dedent(text)
380 return textwrap.dedent(text)
381
381
382 first, rest = splits
382 first, rest = splits
383 # dedent everything but the first line
383 # dedent everything but the first line
384 rest = textwrap.dedent(rest)
384 rest = textwrap.dedent(rest)
385 return '\n'.join([first, rest])
385 return '\n'.join([first, rest])
386
386
387
387
388 def wrap_paragraphs(text, ncols=80):
388 def wrap_paragraphs(text, ncols=80):
389 """Wrap multiple paragraphs to fit a specified width.
389 """Wrap multiple paragraphs to fit a specified width.
390
390
391 This is equivalent to textwrap.wrap, but with support for multiple
391 This is equivalent to textwrap.wrap, but with support for multiple
392 paragraphs, as separated by empty lines.
392 paragraphs, as separated by empty lines.
393
393
394 Returns
394 Returns
395 -------
395 -------
396
396
397 list of complete paragraphs, wrapped to fill `ncols` columns.
397 list of complete paragraphs, wrapped to fill `ncols` columns.
398 """
398 """
399 paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE)
399 paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE)
400 text = dedent(text).strip()
400 text = dedent(text).strip()
401 paragraphs = paragraph_re.split(text)[::2] # every other entry is space
401 paragraphs = paragraph_re.split(text)[::2] # every other entry is space
402 out_ps = []
402 out_ps = []
403 indent_re = re.compile(r'\n\s+', re.MULTILINE)
403 indent_re = re.compile(r'\n\s+', re.MULTILINE)
404 for p in paragraphs:
404 for p in paragraphs:
405 # presume indentation that survives dedent is meaningful formatting,
405 # presume indentation that survives dedent is meaningful formatting,
406 # so don't fill unless text is flush.
406 # so don't fill unless text is flush.
407 if indent_re.search(p) is None:
407 if indent_re.search(p) is None:
408 # wrap paragraph
408 # wrap paragraph
409 p = textwrap.fill(p, ncols)
409 p = textwrap.fill(p, ncols)
410 out_ps.append(p)
410 out_ps.append(p)
411 return out_ps
411 return out_ps
412
412
413
413
414 def long_substr(data):
414 def long_substr(data):
415 """Return the longest common substring in a list of strings.
415 """Return the longest common substring in a list of strings.
416
416
417 Credit: http://stackoverflow.com/questions/2892931/longest-common-substring-from-more-than-two-strings-python
417 Credit: http://stackoverflow.com/questions/2892931/longest-common-substring-from-more-than-two-strings-python
418 """
418 """
419 substr = ''
419 substr = ''
420 if len(data) > 1 and len(data[0]) > 0:
420 if len(data) > 1 and len(data[0]) > 0:
421 for i in range(len(data[0])):
421 for i in range(len(data[0])):
422 for j in range(len(data[0])-i+1):
422 for j in range(len(data[0])-i+1):
423 if j > len(substr) and all(data[0][i:i+j] in x for x in data):
423 if j > len(substr) and all(data[0][i:i+j] in x for x in data):
424 substr = data[0][i:i+j]
424 substr = data[0][i:i+j]
425 elif len(data) == 1:
425 elif len(data) == 1:
426 substr = data[0]
426 substr = data[0]
427 return substr
427 return substr
428
428
429
429
430 def strip_email_quotes(text):
430 def strip_email_quotes(text):
431 """Strip leading email quotation characters ('>').
431 """Strip leading email quotation characters ('>').
432
432
433 Removes any combination of leading '>' interspersed with whitespace that
433 Removes any combination of leading '>' interspersed with whitespace that
434 appears *identically* in all lines of the input text.
434 appears *identically* in all lines of the input text.
435
435
436 Parameters
436 Parameters
437 ----------
437 ----------
438 text : str
438 text : str
439
439
440 Examples
440 Examples
441 --------
441 --------
442
442
443 Simple uses::
443 Simple uses::
444
444
445 In [2]: strip_email_quotes('> > text')
445 In [2]: strip_email_quotes('> > text')
446 Out[2]: 'text'
446 Out[2]: 'text'
447
447
448 In [3]: strip_email_quotes('> > text\\n> > more')
448 In [3]: strip_email_quotes('> > text\\n> > more')
449 Out[3]: 'text\\nmore'
449 Out[3]: 'text\\nmore'
450
450
451 Note how only the common prefix that appears in all lines is stripped::
451 Note how only the common prefix that appears in all lines is stripped::
452
452
453 In [4]: strip_email_quotes('> > text\\n> > more\\n> more...')
453 In [4]: strip_email_quotes('> > text\\n> > more\\n> more...')
454 Out[4]: '> text\\n> more\\nmore...'
454 Out[4]: '> text\\n> more\\nmore...'
455
455
456 So if any line has no quote marks ('>') , then none are stripped from any
456 So if any line has no quote marks ('>') , then none are stripped from any
457 of them ::
457 of them ::
458
458
459 In [5]: strip_email_quotes('> > text\\n> > more\\nlast different')
459 In [5]: strip_email_quotes('> > text\\n> > more\\nlast different')
460 Out[5]: '> > text\\n> > more\\nlast different'
460 Out[5]: '> > text\\n> > more\\nlast different'
461 """
461 """
462 lines = text.splitlines()
462 lines = text.splitlines()
463 matches = set()
463 matches = set()
464 for line in lines:
464 for line in lines:
465 prefix = re.match(r'^(\s*>[ >]*)', line)
465 prefix = re.match(r'^(\s*>[ >]*)', line)
466 if prefix:
466 if prefix:
467 matches.add(prefix.group(1))
467 matches.add(prefix.group(1))
468 else:
468 else:
469 break
469 break
470 else:
470 else:
471 prefix = long_substr(list(matches))
471 prefix = long_substr(list(matches))
472 if prefix:
472 if prefix:
473 strip = len(prefix)
473 strip = len(prefix)
474 text = '\n'.join([ ln[strip:] for ln in lines])
474 text = '\n'.join([ ln[strip:] for ln in lines])
475 return text
475 return text
476
476
477 def strip_ansi(source):
477 def strip_ansi(source):
478 """
478 """
479 Remove ansi escape codes from text.
479 Remove ansi escape codes from text.
480
480
481 Parameters
481 Parameters
482 ----------
482 ----------
483 source : str
483 source : str
484 Source to remove the ansi from
484 Source to remove the ansi from
485 """
485 """
486 return re.sub(r'\033\[(\d|;)+?m', '', source)
486 return re.sub(r'\033\[(\d|;)+?m', '', source)
487
487
488
488
489 class EvalFormatter(Formatter):
489 class EvalFormatter(Formatter):
490 """A String Formatter that allows evaluation of simple expressions.
490 """A String Formatter that allows evaluation of simple expressions.
491
491
492 Note that this version interprets a : as specifying a format string (as per
492 Note that this version interprets a : as specifying a format string (as per
493 standard string formatting), so if slicing is required, you must explicitly
493 standard string formatting), so if slicing is required, you must explicitly
494 create a slice.
494 create a slice.
495
495
496 This is to be used in templating cases, such as the parallel batch
496 This is to be used in templating cases, such as the parallel batch
497 script templates, where simple arithmetic on arguments is useful.
497 script templates, where simple arithmetic on arguments is useful.
498
498
499 Examples
499 Examples
500 --------
500 --------
501 ::
501 ::
502
502
503 In [1]: f = EvalFormatter()
503 In [1]: f = EvalFormatter()
504 In [2]: f.format('{n//4}', n=8)
504 In [2]: f.format('{n//4}', n=8)
505 Out[2]: '2'
505 Out[2]: '2'
506
506
507 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
507 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
508 Out[3]: 'll'
508 Out[3]: 'll'
509 """
509 """
510 def get_field(self, name, args, kwargs):
510 def get_field(self, name, args, kwargs):
511 v = eval(name, kwargs)
511 v = eval(name, kwargs)
512 return v, name
512 return v, name
513
513
514 #XXX: As of Python 3.4, the format string parsing no longer splits on a colon
514 #XXX: As of Python 3.4, the format string parsing no longer splits on a colon
515 # inside [], so EvalFormatter can handle slicing. Once we only support 3.4 and
515 # inside [], so EvalFormatter can handle slicing. Once we only support 3.4 and
516 # above, it should be possible to remove FullEvalFormatter.
516 # above, it should be possible to remove FullEvalFormatter.
517
517
518 class FullEvalFormatter(Formatter):
518 class FullEvalFormatter(Formatter):
519 """A String Formatter that allows evaluation of simple expressions.
519 """A String Formatter that allows evaluation of simple expressions.
520
520
521 Any time a format key is not found in the kwargs,
521 Any time a format key is not found in the kwargs,
522 it will be tried as an expression in the kwargs namespace.
522 it will be tried as an expression in the kwargs namespace.
523
523
524 Note that this version allows slicing using [1:2], so you cannot specify
524 Note that this version allows slicing using [1:2], so you cannot specify
525 a format string. Use :class:`EvalFormatter` to permit format strings.
525 a format string. Use :class:`EvalFormatter` to permit format strings.
526
526
527 Examples
527 Examples
528 --------
528 --------
529 ::
529 ::
530
530
531 In [1]: f = FullEvalFormatter()
531 In [1]: f = FullEvalFormatter()
532 In [2]: f.format('{n//4}', n=8)
532 In [2]: f.format('{n//4}', n=8)
533 Out[2]: '2'
533 Out[2]: '2'
534
534
535 In [3]: f.format('{list(range(5))[2:4]}')
535 In [3]: f.format('{list(range(5))[2:4]}')
536 Out[3]: '[2, 3]'
536 Out[3]: '[2, 3]'
537
537
538 In [4]: f.format('{3*2}')
538 In [4]: f.format('{3*2}')
539 Out[4]: '6'
539 Out[4]: '6'
540 """
540 """
541 # copied from Formatter._vformat with minor changes to allow eval
541 # copied from Formatter._vformat with minor changes to allow eval
542 # and replace the format_spec code with slicing
542 # and replace the format_spec code with slicing
543 def vformat(self, format_string, args, kwargs):
543 def vformat(self, format_string, args, kwargs):
544 result = []
544 result = []
545 for literal_text, field_name, format_spec, conversion in \
545 for literal_text, field_name, format_spec, conversion in \
546 self.parse(format_string):
546 self.parse(format_string):
547
547
548 # output the literal text
548 # output the literal text
549 if literal_text:
549 if literal_text:
550 result.append(literal_text)
550 result.append(literal_text)
551
551
552 # if there's a field, output it
552 # if there's a field, output it
553 if field_name is not None:
553 if field_name is not None:
554 # this is some markup, find the object and do
554 # this is some markup, find the object and do
555 # the formatting
555 # the formatting
556
556
557 if format_spec:
557 if format_spec:
558 # override format spec, to allow slicing:
558 # override format spec, to allow slicing:
559 field_name = ':'.join([field_name, format_spec])
559 field_name = ':'.join([field_name, format_spec])
560
560
561 # eval the contents of the field for the object
561 # eval the contents of the field for the object
562 # to be formatted
562 # to be formatted
563 obj = eval(field_name, kwargs)
563 obj = eval(field_name, kwargs)
564
564
565 # do any conversion on the resulting object
565 # do any conversion on the resulting object
566 obj = self.convert_field(obj, conversion)
566 obj = self.convert_field(obj, conversion)
567
567
568 # format the object and append to the result
568 # format the object and append to the result
569 result.append(self.format_field(obj, ''))
569 result.append(self.format_field(obj, ''))
570
570
571 return u''.join(py3compat.cast_unicode(s) for s in result)
571 return u''.join(py3compat.cast_unicode(s) for s in result)
572
572
573
573
574 class DollarFormatter(FullEvalFormatter):
574 class DollarFormatter(FullEvalFormatter):
575 """Formatter allowing Itpl style $foo replacement, for names and attribute
575 """Formatter allowing Itpl style $foo replacement, for names and attribute
576 access only. Standard {foo} replacement also works, and allows full
576 access only. Standard {foo} replacement also works, and allows full
577 evaluation of its arguments.
577 evaluation of its arguments.
578
578
579 Examples
579 Examples
580 --------
580 --------
581 ::
581 ::
582
582
583 In [1]: f = DollarFormatter()
583 In [1]: f = DollarFormatter()
584 In [2]: f.format('{n//4}', n=8)
584 In [2]: f.format('{n//4}', n=8)
585 Out[2]: '2'
585 Out[2]: '2'
586
586
587 In [3]: f.format('23 * 76 is $result', result=23*76)
587 In [3]: f.format('23 * 76 is $result', result=23*76)
588 Out[3]: '23 * 76 is 1748'
588 Out[3]: '23 * 76 is 1748'
589
589
590 In [4]: f.format('$a or {b}', a=1, b=2)
590 In [4]: f.format('$a or {b}', a=1, b=2)
591 Out[4]: '1 or 2'
591 Out[4]: '1 or 2'
592 """
592 """
593 _dollar_pattern = re.compile("(.*?)\$(\$?[\w\.]+)")
593 _dollar_pattern = re.compile("(.*?)\$(\$?[\w\.]+)")
594 def parse(self, fmt_string):
594 def parse(self, fmt_string):
595 for literal_txt, field_name, format_spec, conversion \
595 for literal_txt, field_name, format_spec, conversion \
596 in Formatter.parse(self, fmt_string):
596 in Formatter.parse(self, fmt_string):
597
597
598 # Find $foo patterns in the literal text.
598 # Find $foo patterns in the literal text.
599 continue_from = 0
599 continue_from = 0
600 txt = ""
600 txt = ""
601 for m in self._dollar_pattern.finditer(literal_txt):
601 for m in self._dollar_pattern.finditer(literal_txt):
602 new_txt, new_field = m.group(1,2)
602 new_txt, new_field = m.group(1,2)
603 # $$foo --> $foo
603 # $$foo --> $foo
604 if new_field.startswith("$"):
604 if new_field.startswith("$"):
605 txt += new_txt + new_field
605 txt += new_txt + new_field
606 else:
606 else:
607 yield (txt + new_txt, new_field, "", None)
607 yield (txt + new_txt, new_field, "", None)
608 txt = ""
608 txt = ""
609 continue_from = m.end()
609 continue_from = m.end()
610
610
611 # Re-yield the {foo} style pattern
611 # Re-yield the {foo} style pattern
612 yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
612 yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
613
613
614 #-----------------------------------------------------------------------------
614 #-----------------------------------------------------------------------------
615 # Utils to columnize a list of string
615 # Utils to columnize a list of string
616 #-----------------------------------------------------------------------------
616 #-----------------------------------------------------------------------------
617
617
618 def _col_chunks(l, max_rows, row_first=False):
618 def _col_chunks(l, max_rows, row_first=False):
619 """Yield successive max_rows-sized column chunks from l."""
619 """Yield successive max_rows-sized column chunks from l."""
620 if row_first:
620 if row_first:
621 ncols = (len(l) // max_rows) + (len(l) % max_rows > 0)
621 ncols = (len(l) // max_rows) + (len(l) % max_rows > 0)
622 for i in py3compat.xrange(ncols):
622 for i in py3compat.xrange(ncols):
623 yield [l[j] for j in py3compat.xrange(i, len(l), ncols)]
623 yield [l[j] for j in py3compat.xrange(i, len(l), ncols)]
624 else:
624 else:
625 for i in py3compat.xrange(0, len(l), max_rows):
625 for i in py3compat.xrange(0, len(l), max_rows):
626 yield l[i:(i + max_rows)]
626 yield l[i:(i + max_rows)]
627
627
628
628
629 def _find_optimal(rlist, row_first=False, separator_size=2, displaywidth=80):
629 def _find_optimal(rlist, row_first=False, separator_size=2, displaywidth=80):
630 """Calculate optimal info to columnize a list of string"""
630 """Calculate optimal info to columnize a list of string"""
631 for max_rows in range(1, len(rlist) + 1):
631 for max_rows in range(1, len(rlist) + 1):
632 col_widths = list(map(max, _col_chunks(rlist, max_rows, row_first)))
632 col_widths = list(map(max, _col_chunks(rlist, max_rows, row_first)))
633 sumlength = sum(col_widths)
633 sumlength = sum(col_widths)
634 ncols = len(col_widths)
634 ncols = len(col_widths)
635 if sumlength + separator_size * (ncols - 1) <= displaywidth:
635 if sumlength + separator_size * (ncols - 1) <= displaywidth:
636 break
636 break
637 return {'num_columns': ncols,
637 return {'num_columns': ncols,
638 'optimal_separator_width': (displaywidth - sumlength) / (ncols - 1) if (ncols - 1) else 0,
638 'optimal_separator_width': (displaywidth - sumlength) // (ncols - 1) if (ncols - 1) else 0,
639 'max_rows': max_rows,
639 'max_rows': max_rows,
640 'column_widths': col_widths
640 'column_widths': col_widths
641 }
641 }
642
642
643
643
644 def _get_or_default(mylist, i, default=None):
644 def _get_or_default(mylist, i, default=None):
645 """return list item number, or default if don't exist"""
645 """return list item number, or default if don't exist"""
646 if i >= len(mylist):
646 if i >= len(mylist):
647 return default
647 return default
648 else :
648 else :
649 return mylist[i]
649 return mylist[i]
650
650
651
651
652 def compute_item_matrix(items, row_first=False, empty=None, *args, **kwargs) :
652 def compute_item_matrix(items, row_first=False, empty=None, *args, **kwargs) :
653 """Returns a nested list, and info to columnize items
653 """Returns a nested list, and info to columnize items
654
654
655 Parameters
655 Parameters
656 ----------
656 ----------
657
657
658 items
658 items
659 list of strings to columize
659 list of strings to columize
660 row_first : (default False)
660 row_first : (default False)
661 Whether to compute columns for a row-first matrix instead of
661 Whether to compute columns for a row-first matrix instead of
662 column-first (default).
662 column-first (default).
663 empty : (default None)
663 empty : (default None)
664 default value to fill list if needed
664 default value to fill list if needed
665 separator_size : int (default=2)
665 separator_size : int (default=2)
666 How much caracters will be used as a separation between each columns.
666 How much caracters will be used as a separation between each columns.
667 displaywidth : int (default=80)
667 displaywidth : int (default=80)
668 The width of the area onto wich the columns should enter
668 The width of the area onto wich the columns should enter
669
669
670 Returns
670 Returns
671 -------
671 -------
672
672
673 strings_matrix
673 strings_matrix
674
674
675 nested list of string, the outer most list contains as many list as
675 nested list of string, the outer most list contains as many list as
676 rows, the innermost lists have each as many element as colums. If the
676 rows, the innermost lists have each as many element as colums. If the
677 total number of elements in `items` does not equal the product of
677 total number of elements in `items` does not equal the product of
678 rows*columns, the last element of some lists are filled with `None`.
678 rows*columns, the last element of some lists are filled with `None`.
679
679
680 dict_info
680 dict_info
681 some info to make columnize easier:
681 some info to make columnize easier:
682
682
683 num_columns
683 num_columns
684 number of columns
684 number of columns
685 max_rows
685 max_rows
686 maximum number of rows (final number may be less)
686 maximum number of rows (final number may be less)
687 column_widths
687 column_widths
688 list of with of each columns
688 list of with of each columns
689 optimal_separator_width
689 optimal_separator_width
690 best separator width between columns
690 best separator width between columns
691
691
692 Examples
692 Examples
693 --------
693 --------
694 ::
694 ::
695
695
696 In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l']
696 In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l']
697 ...: compute_item_matrix(l, displaywidth=12)
697 In [2]: list, info = compute_item_matrix(l, displaywidth=12)
698 Out[1]:
698 In [3]: list
699 ([['aaa', 'f', 'k'],
699 Out[3]: [['aaa', 'f', 'k'], ['b', 'g', 'l'], ['cc', 'h', None], ['d', 'i', None], ['eeeee', 'j', None]]
700 ['b', 'g', 'l'],
700 In [4]: ideal = {'num_columns': 3, 'column_widths': [5, 1, 1], 'optimal_separator_width': 2, 'max_rows': 5}
701 ['cc', 'h', None],
701 In [5]: all((info[k] == ideal[k] for k in ideal.keys()))
702 ['d', 'i', None],
702 Out[5]: True
703 ['eeeee', 'j', None]],
704 {'num_columns': 3,
705 'column_widths': [5, 1, 1],
706 'optimal_separator_width': 2,
707 'max_rows': 5})
708 """
703 """
709 info = _find_optimal(list(map(len, items)), row_first, *args, **kwargs)
704 info = _find_optimal(list(map(len, items)), row_first, *args, **kwargs)
710 nrow, ncol = info['max_rows'], info['num_columns']
705 nrow, ncol = info['max_rows'], info['num_columns']
711 if row_first:
706 if row_first:
712 return ([[_get_or_default(items, r * ncol + c, default=empty) for c in range(ncol)] for r in range(nrow)], info)
707 return ([[_get_or_default(items, r * ncol + c, default=empty) for c in range(ncol)] for r in range(nrow)], info)
713 else:
708 else:
714 return ([[_get_or_default(items, c * nrow + r, default=empty) for c in range(ncol)] for r in range(nrow)], info)
709 return ([[_get_or_default(items, c * nrow + r, default=empty) for c in range(ncol)] for r in range(nrow)], info)
715
710
716
711
717 def columnize(items, row_first=False, separator=' ', displaywidth=80, spread=False):
712 def columnize(items, row_first=False, separator=' ', displaywidth=80, spread=False):
718 """ Transform a list of strings into a single string with columns.
713 """ Transform a list of strings into a single string with columns.
719
714
720 Parameters
715 Parameters
721 ----------
716 ----------
722 items : sequence of strings
717 items : sequence of strings
723 The strings to process.
718 The strings to process.
724
719
725 row_first : (default False)
720 row_first : (default False)
726 Whether to compute columns for a row-first matrix instead of
721 Whether to compute columns for a row-first matrix instead of
727 column-first (default).
722 column-first (default).
728
723
729 separator : str, optional [default is two spaces]
724 separator : str, optional [default is two spaces]
730 The string that separates columns.
725 The string that separates columns.
731
726
732 displaywidth : int, optional [default is 80]
727 displaywidth : int, optional [default is 80]
733 Width of the display in number of characters.
728 Width of the display in number of characters.
734
729
735 Returns
730 Returns
736 -------
731 -------
737 The formatted string.
732 The formatted string.
738 """
733 """
739 if not items:
734 if not items:
740 return '\n'
735 return '\n'
741 matrix, info = compute_item_matrix(items, row_first=row_first, separator_size=len(separator), displaywidth=displaywidth)
736 matrix, info = compute_item_matrix(items, row_first=row_first, separator_size=len(separator), displaywidth=displaywidth)
742 if spread:
737 if spread:
743 separator = separator.ljust(int(info['optimal_separator_width']))
738 separator = separator.ljust(int(info['optimal_separator_width']))
744 fmatrix = [filter(None, x) for x in matrix]
739 fmatrix = [filter(None, x) for x in matrix]
745 sjoin = lambda x : separator.join([ y.ljust(w, ' ') for y, w in zip(x, info['column_widths'])])
740 sjoin = lambda x : separator.join([ y.ljust(w, ' ') for y, w in zip(x, info['column_widths'])])
746 return '\n'.join(map(sjoin, fmatrix))+'\n'
741 return '\n'.join(map(sjoin, fmatrix))+'\n'
747
742
748
743
749 def get_text_list(list_, last_sep=' and ', sep=", ", wrap_item_with=""):
744 def get_text_list(list_, last_sep=' and ', sep=", ", wrap_item_with=""):
750 """
745 """
751 Return a string with a natural enumeration of items
746 Return a string with a natural enumeration of items
752
747
753 >>> get_text_list(['a', 'b', 'c', 'd'])
748 >>> get_text_list(['a', 'b', 'c', 'd'])
754 'a, b, c and d'
749 'a, b, c and d'
755 >>> get_text_list(['a', 'b', 'c'], ' or ')
750 >>> get_text_list(['a', 'b', 'c'], ' or ')
756 'a, b or c'
751 'a, b or c'
757 >>> get_text_list(['a', 'b', 'c'], ', ')
752 >>> get_text_list(['a', 'b', 'c'], ', ')
758 'a, b, c'
753 'a, b, c'
759 >>> get_text_list(['a', 'b'], ' or ')
754 >>> get_text_list(['a', 'b'], ' or ')
760 'a or b'
755 'a or b'
761 >>> get_text_list(['a'])
756 >>> get_text_list(['a'])
762 'a'
757 'a'
763 >>> get_text_list([])
758 >>> get_text_list([])
764 ''
759 ''
765 >>> get_text_list(['a', 'b'], wrap_item_with="`")
760 >>> get_text_list(['a', 'b'], wrap_item_with="`")
766 '`a` and `b`'
761 '`a` and `b`'
767 >>> get_text_list(['a', 'b', 'c', 'd'], " = ", sep=" + ")
762 >>> get_text_list(['a', 'b', 'c', 'd'], " = ", sep=" + ")
768 'a + b + c = d'
763 'a + b + c = d'
769 """
764 """
770 if len(list_) == 0:
765 if len(list_) == 0:
771 return ''
766 return ''
772 if wrap_item_with:
767 if wrap_item_with:
773 list_ = ['%s%s%s' % (wrap_item_with, item, wrap_item_with) for
768 list_ = ['%s%s%s' % (wrap_item_with, item, wrap_item_with) for
774 item in list_]
769 item in list_]
775 if len(list_) == 1:
770 if len(list_) == 1:
776 return list_[0]
771 return list_[0]
777 return '%s%s%s' % (
772 return '%s%s%s' % (
778 sep.join(i for i in list_[:-1]),
773 sep.join(i for i in list_[:-1]),
779 last_sep, list_[-1])
774 last_sep, list_[-1])
General Comments 0
You need to be logged in to leave comments. Login now