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