##// END OF EJS Templates
Move strip_ansi function to utils
Thomas Kluyver -
Show More
@@ -1,178 +1,166 b''
1 """Filters for processing ANSI colors within Jinja templates.
1 """Filters for processing ANSI colors within Jinja templates.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Copyright (c) 2013, the IPython Development Team.
4 # Copyright (c) 2013, the IPython Development Team.
5 #
5 #
6 # Distributed under the terms of the Modified BSD License.
6 # Distributed under the terms of the Modified BSD License.
7 #
7 #
8 # The full license is in the file COPYING.txt, distributed with this software.
8 # The full license is in the file COPYING.txt, distributed with this software.
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Imports
12 # Imports
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 import re
15 import re
16 from IPython.utils import coloransi
16 from IPython.utils import coloransi
17 from IPython.utils.text import strip_ansi
17
18
18 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
19 # Classes and functions
20 # Classes and functions
20 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
21
22
22 __all__ = [
23 __all__ = [
23 'strip_ansi',
24 'strip_ansi',
24 'ansi2html',
25 'ansi2html',
25 'single_ansi2latex',
26 'single_ansi2latex',
26 'ansi2latex'
27 'ansi2latex'
27 ]
28 ]
28
29
29 def strip_ansi(source):
30 """
31 Remove ansi from text
32
33 Parameters
34 ----------
35 source : str
36 Source to remove the ansi from
37 """
38
39 return re.sub(r'\033\[(\d|;)+?m', '', source)
40
41
42 ansi_colormap = {
30 ansi_colormap = {
43 '30': 'ansiblack',
31 '30': 'ansiblack',
44 '31': 'ansired',
32 '31': 'ansired',
45 '32': 'ansigreen',
33 '32': 'ansigreen',
46 '33': 'ansiyellow',
34 '33': 'ansiyellow',
47 '34': 'ansiblue',
35 '34': 'ansiblue',
48 '35': 'ansipurple',
36 '35': 'ansipurple',
49 '36': 'ansicyan',
37 '36': 'ansicyan',
50 '37': 'ansigrey',
38 '37': 'ansigrey',
51 '01': 'ansibold',
39 '01': 'ansibold',
52 }
40 }
53
41
54 html_escapes = {
42 html_escapes = {
55 '<': '&lt;',
43 '<': '&lt;',
56 '>': '&gt;',
44 '>': '&gt;',
57 "'": '&apos;',
45 "'": '&apos;',
58 '"': '&quot;',
46 '"': '&quot;',
59 '`': '&#96;',
47 '`': '&#96;',
60 }
48 }
61 ansi_re = re.compile('\x1b' + r'\[([\dA-Fa-f;]*?)m')
49 ansi_re = re.compile('\x1b' + r'\[([\dA-Fa-f;]*?)m')
62
50
63 def ansi2html(text):
51 def ansi2html(text):
64 """
52 """
65 Convert ansi colors to html colors.
53 Convert ansi colors to html colors.
66
54
67 Parameters
55 Parameters
68 ----------
56 ----------
69 text : str
57 text : str
70 Text containing ansi colors to convert to html
58 Text containing ansi colors to convert to html
71 """
59 """
72
60
73 # do ampersand first
61 # do ampersand first
74 text = text.replace('&', '&amp;')
62 text = text.replace('&', '&amp;')
75
63
76 for c, escape in html_escapes.items():
64 for c, escape in html_escapes.items():
77 text = text.replace(c, escape)
65 text = text.replace(c, escape)
78
66
79 m = ansi_re.search(text)
67 m = ansi_re.search(text)
80 opened = False
68 opened = False
81 cmds = []
69 cmds = []
82 opener = ''
70 opener = ''
83 closer = ''
71 closer = ''
84 while m:
72 while m:
85 cmds = m.groups()[0].split(';')
73 cmds = m.groups()[0].split(';')
86 closer = '</span>' if opened else ''
74 closer = '</span>' if opened else ''
87
75
88 # True if there is there more than one element in cmds, *or*
76 # True if there is there more than one element in cmds, *or*
89 # if there is only one but it is not equal to a string of zeroes.
77 # if there is only one but it is not equal to a string of zeroes.
90 opened = len(cmds) > 1 or cmds[0] != '0' * len(cmds[0])
78 opened = len(cmds) > 1 or cmds[0] != '0' * len(cmds[0])
91 classes = []
79 classes = []
92 for cmd in cmds:
80 for cmd in cmds:
93 if cmd in ansi_colormap:
81 if cmd in ansi_colormap:
94 classes.append(ansi_colormap[cmd])
82 classes.append(ansi_colormap[cmd])
95
83
96 if classes:
84 if classes:
97 opener = '<span class="%s">' % (' '.join(classes))
85 opener = '<span class="%s">' % (' '.join(classes))
98 else:
86 else:
99 opener = ''
87 opener = ''
100 text = re.sub(ansi_re, closer + opener, text, 1)
88 text = re.sub(ansi_re, closer + opener, text, 1)
101
89
102 m = ansi_re.search(text)
90 m = ansi_re.search(text)
103
91
104 if opened:
92 if opened:
105 text += '</span>'
93 text += '</span>'
106 return text
94 return text
107
95
108
96
109 def single_ansi2latex(code):
97 def single_ansi2latex(code):
110 """Converts single ansi markup to latex format.
98 """Converts single ansi markup to latex format.
111
99
112 Return latex code and number of open brackets.
100 Return latex code and number of open brackets.
113
101
114 Accepts codes like '\x1b[1;32m' (bold, red) and the short form '\x1b[32m' (red)
102 Accepts codes like '\x1b[1;32m' (bold, red) and the short form '\x1b[32m' (red)
115
103
116 Colors are matched to those defined in coloransi, which defines colors
104 Colors are matched to those defined in coloransi, which defines colors
117 using the 0, 1 (bold) and 5 (blinking) styles. Styles 1 and 5 are
105 using the 0, 1 (bold) and 5 (blinking) styles. Styles 1 and 5 are
118 interpreted as bold. All other styles are mapped to 0. Note that in
106 interpreted as bold. All other styles are mapped to 0. Note that in
119 coloransi, a style of 1 does not just mean bold; for example, Brown is
107 coloransi, a style of 1 does not just mean bold; for example, Brown is
120 "0;33", but Yellow is "1;33". An empty string is returned for unrecognised
108 "0;33", but Yellow is "1;33". An empty string is returned for unrecognised
121 codes and the "reset" code '\x1b[m'.
109 codes and the "reset" code '\x1b[m'.
122 """
110 """
123 components = code.split(';')
111 components = code.split(';')
124 if len(components) > 1:
112 if len(components) > 1:
125 # Style is digits after '['
113 # Style is digits after '['
126 style = int(components[0].split('[')[-1])
114 style = int(components[0].split('[')[-1])
127 color = components[1][:-1]
115 color = components[1][:-1]
128 else:
116 else:
129 style = 0
117 style = 0
130 color = components[0][-3:-1]
118 color = components[0][-3:-1]
131
119
132 # If the style is not normal (0), bold (1) or blinking (5) then treat it as normal
120 # If the style is not normal (0), bold (1) or blinking (5) then treat it as normal
133 if style not in [0, 1, 5]:
121 if style not in [0, 1, 5]:
134 style = 0
122 style = 0
135
123
136 for name, tcode in coloransi.color_templates:
124 for name, tcode in coloransi.color_templates:
137 tstyle, tcolor = tcode.split(';')
125 tstyle, tcolor = tcode.split(';')
138 tstyle = int(tstyle)
126 tstyle = int(tstyle)
139 if tstyle == style and tcolor == color:
127 if tstyle == style and tcolor == color:
140 break
128 break
141 else:
129 else:
142 return '', 0
130 return '', 0
143
131
144 if style == 5:
132 if style == 5:
145 name = name[5:] # BlinkRed -> Red, etc
133 name = name[5:] # BlinkRed -> Red, etc
146 name = name.lower()
134 name = name.lower()
147
135
148 if style in [1, 5]:
136 if style in [1, 5]:
149 return r'\textbf{\color{'+name+'}', 1
137 return r'\textbf{\color{'+name+'}', 1
150 else:
138 else:
151 return r'{\color{'+name+'}', 1
139 return r'{\color{'+name+'}', 1
152
140
153 def ansi2latex(text):
141 def ansi2latex(text):
154 """Converts ansi formated text to latex version
142 """Converts ansi formated text to latex version
155
143
156 based on https://bitbucket.org/birkenfeld/sphinx-contrib/ansi.py
144 based on https://bitbucket.org/birkenfeld/sphinx-contrib/ansi.py
157 """
145 """
158 color_pattern = re.compile('\x1b\\[([^m]*)m')
146 color_pattern = re.compile('\x1b\\[([^m]*)m')
159 last_end = 0
147 last_end = 0
160 openbrack = 0
148 openbrack = 0
161 outstring = ''
149 outstring = ''
162 for match in color_pattern.finditer(text):
150 for match in color_pattern.finditer(text):
163 head = text[last_end:match.start()]
151 head = text[last_end:match.start()]
164 outstring += head
152 outstring += head
165 if openbrack:
153 if openbrack:
166 outstring += '}'*openbrack
154 outstring += '}'*openbrack
167 openbrack = 0
155 openbrack = 0
168 code = match.group()
156 code = match.group()
169 if not (code == coloransi.TermColors.Normal or openbrack):
157 if not (code == coloransi.TermColors.Normal or openbrack):
170 texform, openbrack = single_ansi2latex(code)
158 texform, openbrack = single_ansi2latex(code)
171 outstring += texform
159 outstring += texform
172 last_end = match.end()
160 last_end = match.end()
173
161
174 # Add the remainer of the string and THEN close any remaining color brackets.
162 # Add the remainer of the string and THEN close any remaining color brackets.
175 outstring += text[last_end:]
163 outstring += text[last_end:]
176 if openbrack:
164 if openbrack:
177 outstring += '}'*openbrack
165 outstring += '}'*openbrack
178 return outstring.strip()
166 return outstring.strip()
@@ -1,672 +1,672 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Process Controller
2 """IPython Test Process Controller
3
3
4 This module runs one or more subprocesses which will actually run the IPython
4 This module runs one or more subprocesses which will actually run the IPython
5 test suite.
5 test suite.
6
6
7 """
7 """
8
8
9 # Copyright (c) IPython Development Team.
9 # Copyright (c) IPython Development Team.
10 # Distributed under the terms of the Modified BSD License.
10 # Distributed under the terms of the Modified BSD License.
11
11
12 from __future__ import print_function
12 from __future__ import print_function
13
13
14 import argparse
14 import argparse
15 import json
15 import json
16 import multiprocessing.pool
16 import multiprocessing.pool
17 import os
17 import os
18 import shutil
18 import shutil
19 import signal
19 import signal
20 import sys
20 import sys
21 import subprocess
21 import subprocess
22 import time
22 import time
23 import re
23 import re
24
24
25 from .iptest import have, test_group_names as py_test_group_names, test_sections, StreamCapturer
25 from .iptest import have, test_group_names as py_test_group_names, test_sections, StreamCapturer
26 from IPython.utils.path import compress_user
26 from IPython.utils.path import compress_user
27 from IPython.utils.py3compat import bytes_to_str
27 from IPython.utils.py3compat import bytes_to_str
28 from IPython.utils.sysinfo import get_sys_info
28 from IPython.utils.sysinfo import get_sys_info
29 from IPython.utils.tempdir import TemporaryDirectory
29 from IPython.utils.tempdir import TemporaryDirectory
30 from IPython.nbconvert.filters.ansi import strip_ansi
30 from IPython.utils.text import strip_ansi
31
31
32 try:
32 try:
33 # Python >= 3.3
33 # Python >= 3.3
34 from subprocess import TimeoutExpired
34 from subprocess import TimeoutExpired
35 def popen_wait(p, timeout):
35 def popen_wait(p, timeout):
36 return p.wait(timeout)
36 return p.wait(timeout)
37 except ImportError:
37 except ImportError:
38 class TimeoutExpired(Exception):
38 class TimeoutExpired(Exception):
39 pass
39 pass
40 def popen_wait(p, timeout):
40 def popen_wait(p, timeout):
41 """backport of Popen.wait from Python 3"""
41 """backport of Popen.wait from Python 3"""
42 for i in range(int(10 * timeout)):
42 for i in range(int(10 * timeout)):
43 if p.poll() is not None:
43 if p.poll() is not None:
44 return
44 return
45 time.sleep(0.1)
45 time.sleep(0.1)
46 if p.poll() is None:
46 if p.poll() is None:
47 raise TimeoutExpired
47 raise TimeoutExpired
48
48
49 NOTEBOOK_SHUTDOWN_TIMEOUT = 10
49 NOTEBOOK_SHUTDOWN_TIMEOUT = 10
50
50
51 class TestController(object):
51 class TestController(object):
52 """Run tests in a subprocess
52 """Run tests in a subprocess
53 """
53 """
54 #: str, IPython test suite to be executed.
54 #: str, IPython test suite to be executed.
55 section = None
55 section = None
56 #: list, command line arguments to be executed
56 #: list, command line arguments to be executed
57 cmd = None
57 cmd = None
58 #: dict, extra environment variables to set for the subprocess
58 #: dict, extra environment variables to set for the subprocess
59 env = None
59 env = None
60 #: list, TemporaryDirectory instances to clear up when the process finishes
60 #: list, TemporaryDirectory instances to clear up when the process finishes
61 dirs = None
61 dirs = None
62 #: subprocess.Popen instance
62 #: subprocess.Popen instance
63 process = None
63 process = None
64 #: str, process stdout+stderr
64 #: str, process stdout+stderr
65 stdout = None
65 stdout = None
66
66
67 def __init__(self):
67 def __init__(self):
68 self.cmd = []
68 self.cmd = []
69 self.env = {}
69 self.env = {}
70 self.dirs = []
70 self.dirs = []
71
71
72 def setup(self):
72 def setup(self):
73 """Create temporary directories etc.
73 """Create temporary directories etc.
74
74
75 This is only called when we know the test group will be run. Things
75 This is only called when we know the test group will be run. Things
76 created here may be cleaned up by self.cleanup().
76 created here may be cleaned up by self.cleanup().
77 """
77 """
78 pass
78 pass
79
79
80 def launch(self, buffer_output=False):
80 def launch(self, buffer_output=False):
81 # print('*** ENV:', self.env) # dbg
81 # print('*** ENV:', self.env) # dbg
82 # print('*** CMD:', self.cmd) # dbg
82 # print('*** CMD:', self.cmd) # dbg
83 env = os.environ.copy()
83 env = os.environ.copy()
84 env.update(self.env)
84 env.update(self.env)
85 output = subprocess.PIPE if buffer_output else None
85 output = subprocess.PIPE if buffer_output else None
86 stdout = subprocess.STDOUT if buffer_output else None
86 stdout = subprocess.STDOUT if buffer_output else None
87 self.process = subprocess.Popen(self.cmd, stdout=output,
87 self.process = subprocess.Popen(self.cmd, stdout=output,
88 stderr=stdout, env=env)
88 stderr=stdout, env=env)
89
89
90 def wait(self):
90 def wait(self):
91 self.stdout, _ = self.process.communicate()
91 self.stdout, _ = self.process.communicate()
92 return self.process.returncode
92 return self.process.returncode
93
93
94 def print_extra_info(self):
94 def print_extra_info(self):
95 """Print extra information about this test run.
95 """Print extra information about this test run.
96
96
97 If we're running in parallel and showing the concise view, this is only
97 If we're running in parallel and showing the concise view, this is only
98 called if the test group fails. Otherwise, it's called before the test
98 called if the test group fails. Otherwise, it's called before the test
99 group is started.
99 group is started.
100
100
101 The base implementation does nothing, but it can be overridden by
101 The base implementation does nothing, but it can be overridden by
102 subclasses.
102 subclasses.
103 """
103 """
104 return
104 return
105
105
106 def cleanup_process(self):
106 def cleanup_process(self):
107 """Cleanup on exit by killing any leftover processes."""
107 """Cleanup on exit by killing any leftover processes."""
108 subp = self.process
108 subp = self.process
109 if subp is None or (subp.poll() is not None):
109 if subp is None or (subp.poll() is not None):
110 return # Process doesn't exist, or is already dead.
110 return # Process doesn't exist, or is already dead.
111
111
112 try:
112 try:
113 print('Cleaning up stale PID: %d' % subp.pid)
113 print('Cleaning up stale PID: %d' % subp.pid)
114 subp.kill()
114 subp.kill()
115 except: # (OSError, WindowsError) ?
115 except: # (OSError, WindowsError) ?
116 # This is just a best effort, if we fail or the process was
116 # This is just a best effort, if we fail or the process was
117 # really gone, ignore it.
117 # really gone, ignore it.
118 pass
118 pass
119 else:
119 else:
120 for i in range(10):
120 for i in range(10):
121 if subp.poll() is None:
121 if subp.poll() is None:
122 time.sleep(0.1)
122 time.sleep(0.1)
123 else:
123 else:
124 break
124 break
125
125
126 if subp.poll() is None:
126 if subp.poll() is None:
127 # The process did not die...
127 # The process did not die...
128 print('... failed. Manual cleanup may be required.')
128 print('... failed. Manual cleanup may be required.')
129
129
130 def cleanup(self):
130 def cleanup(self):
131 "Kill process if it's still alive, and clean up temporary directories"
131 "Kill process if it's still alive, and clean up temporary directories"
132 self.cleanup_process()
132 self.cleanup_process()
133 for td in self.dirs:
133 for td in self.dirs:
134 td.cleanup()
134 td.cleanup()
135
135
136 __del__ = cleanup
136 __del__ = cleanup
137
137
138
138
139 class PyTestController(TestController):
139 class PyTestController(TestController):
140 """Run Python tests using IPython.testing.iptest"""
140 """Run Python tests using IPython.testing.iptest"""
141 #: str, Python command to execute in subprocess
141 #: str, Python command to execute in subprocess
142 pycmd = None
142 pycmd = None
143
143
144 def __init__(self, section, options):
144 def __init__(self, section, options):
145 """Create new test runner."""
145 """Create new test runner."""
146 TestController.__init__(self)
146 TestController.__init__(self)
147 self.section = section
147 self.section = section
148 # pycmd is put into cmd[2] in PyTestController.launch()
148 # pycmd is put into cmd[2] in PyTestController.launch()
149 self.cmd = [sys.executable, '-c', None, section]
149 self.cmd = [sys.executable, '-c', None, section]
150 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
150 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
151 self.options = options
151 self.options = options
152
152
153 def setup(self):
153 def setup(self):
154 ipydir = TemporaryDirectory()
154 ipydir = TemporaryDirectory()
155 self.dirs.append(ipydir)
155 self.dirs.append(ipydir)
156 self.env['IPYTHONDIR'] = ipydir.name
156 self.env['IPYTHONDIR'] = ipydir.name
157 self.workingdir = workingdir = TemporaryDirectory()
157 self.workingdir = workingdir = TemporaryDirectory()
158 self.dirs.append(workingdir)
158 self.dirs.append(workingdir)
159 self.env['IPTEST_WORKING_DIR'] = workingdir.name
159 self.env['IPTEST_WORKING_DIR'] = workingdir.name
160 # This means we won't get odd effects from our own matplotlib config
160 # This means we won't get odd effects from our own matplotlib config
161 self.env['MPLCONFIGDIR'] = workingdir.name
161 self.env['MPLCONFIGDIR'] = workingdir.name
162
162
163 # From options:
163 # From options:
164 if self.options.xunit:
164 if self.options.xunit:
165 self.add_xunit()
165 self.add_xunit()
166 if self.options.coverage:
166 if self.options.coverage:
167 self.add_coverage()
167 self.add_coverage()
168 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
168 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
169 self.cmd.extend(self.options.extra_args)
169 self.cmd.extend(self.options.extra_args)
170
170
171 @property
171 @property
172 def will_run(self):
172 def will_run(self):
173 try:
173 try:
174 return test_sections[self.section].will_run
174 return test_sections[self.section].will_run
175 except KeyError:
175 except KeyError:
176 return True
176 return True
177
177
178 def add_xunit(self):
178 def add_xunit(self):
179 xunit_file = os.path.abspath(self.section + '.xunit.xml')
179 xunit_file = os.path.abspath(self.section + '.xunit.xml')
180 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
180 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
181
181
182 def add_coverage(self):
182 def add_coverage(self):
183 try:
183 try:
184 sources = test_sections[self.section].includes
184 sources = test_sections[self.section].includes
185 except KeyError:
185 except KeyError:
186 sources = ['IPython']
186 sources = ['IPython']
187
187
188 coverage_rc = ("[run]\n"
188 coverage_rc = ("[run]\n"
189 "data_file = {data_file}\n"
189 "data_file = {data_file}\n"
190 "source =\n"
190 "source =\n"
191 " {source}\n"
191 " {source}\n"
192 ).format(data_file=os.path.abspath('.coverage.'+self.section),
192 ).format(data_file=os.path.abspath('.coverage.'+self.section),
193 source="\n ".join(sources))
193 source="\n ".join(sources))
194 config_file = os.path.join(self.workingdir.name, '.coveragerc')
194 config_file = os.path.join(self.workingdir.name, '.coveragerc')
195 with open(config_file, 'w') as f:
195 with open(config_file, 'w') as f:
196 f.write(coverage_rc)
196 f.write(coverage_rc)
197
197
198 self.env['COVERAGE_PROCESS_START'] = config_file
198 self.env['COVERAGE_PROCESS_START'] = config_file
199 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
199 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
200
200
201 def launch(self, buffer_output=False):
201 def launch(self, buffer_output=False):
202 self.cmd[2] = self.pycmd
202 self.cmd[2] = self.pycmd
203 super(PyTestController, self).launch(buffer_output=buffer_output)
203 super(PyTestController, self).launch(buffer_output=buffer_output)
204
204
205
205
206 js_prefix = 'js/'
206 js_prefix = 'js/'
207
207
208 def get_js_test_dir():
208 def get_js_test_dir():
209 import IPython.html.tests as t
209 import IPython.html.tests as t
210 return os.path.join(os.path.dirname(t.__file__), '')
210 return os.path.join(os.path.dirname(t.__file__), '')
211
211
212 def all_js_groups():
212 def all_js_groups():
213 import glob
213 import glob
214 test_dir = get_js_test_dir()
214 test_dir = get_js_test_dir()
215 all_subdirs = glob.glob(test_dir + '[!_]*/')
215 all_subdirs = glob.glob(test_dir + '[!_]*/')
216 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs]
216 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs]
217
217
218 class JSController(TestController):
218 class JSController(TestController):
219 """Run CasperJS tests """
219 """Run CasperJS tests """
220 requirements = ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3']
220 requirements = ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3']
221 display_slimer_output = False
221 display_slimer_output = False
222
222
223 def __init__(self, section, enabled=True, engine='phantomjs'):
223 def __init__(self, section, enabled=True, engine='phantomjs'):
224 """Create new test runner."""
224 """Create new test runner."""
225 TestController.__init__(self)
225 TestController.__init__(self)
226 self.engine = engine
226 self.engine = engine
227 self.section = section
227 self.section = section
228 self.enabled = enabled
228 self.enabled = enabled
229 self.slimer_failure = re.compile('^FAIL.*', flags=re.MULTILINE)
229 self.slimer_failure = re.compile('^FAIL.*', flags=re.MULTILINE)
230 js_test_dir = get_js_test_dir()
230 js_test_dir = get_js_test_dir()
231 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
231 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
232 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
232 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
233 self.cmd = ['casperjs', 'test', includes, test_cases, '--engine=%s' % self.engine]
233 self.cmd = ['casperjs', 'test', includes, test_cases, '--engine=%s' % self.engine]
234
234
235 def setup(self):
235 def setup(self):
236 self.ipydir = TemporaryDirectory()
236 self.ipydir = TemporaryDirectory()
237 self.nbdir = TemporaryDirectory()
237 self.nbdir = TemporaryDirectory()
238 self.dirs.append(self.ipydir)
238 self.dirs.append(self.ipydir)
239 self.dirs.append(self.nbdir)
239 self.dirs.append(self.nbdir)
240 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
240 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
241 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
241 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
242
242
243 # start the ipython notebook, so we get the port number
243 # start the ipython notebook, so we get the port number
244 self.server_port = 0
244 self.server_port = 0
245 self._init_server()
245 self._init_server()
246 if self.server_port:
246 if self.server_port:
247 self.cmd.append("--port=%i" % self.server_port)
247 self.cmd.append("--port=%i" % self.server_port)
248 else:
248 else:
249 # don't launch tests if the server didn't start
249 # don't launch tests if the server didn't start
250 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
250 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
251
251
252 def launch(self, buffer_output):
252 def launch(self, buffer_output):
253 # If the engine is SlimerJS, we need to buffer the output because
253 # If the engine is SlimerJS, we need to buffer the output because
254 # SlimerJS does not support exit codes, so CasperJS always returns 0.
254 # SlimerJS does not support exit codes, so CasperJS always returns 0.
255 if self.engine == 'slimerjs' and not buffer_output:
255 if self.engine == 'slimerjs' and not buffer_output:
256 self.display_slimer_output = True
256 self.display_slimer_output = True
257 return super(JSController, self).launch(buffer_output=True)
257 return super(JSController, self).launch(buffer_output=True)
258
258
259 else:
259 else:
260 return super(JSController, self).launch(buffer_output=buffer_output)
260 return super(JSController, self).launch(buffer_output=buffer_output)
261
261
262 def wait(self, *pargs, **kwargs):
262 def wait(self, *pargs, **kwargs):
263 """Wait for the JSController to finish"""
263 """Wait for the JSController to finish"""
264 ret = super(JSController, self).wait(*pargs, **kwargs)
264 ret = super(JSController, self).wait(*pargs, **kwargs)
265 # If this is a SlimerJS controller, check the captured stdout for
265 # If this is a SlimerJS controller, check the captured stdout for
266 # errors. Otherwise, just return the return code.
266 # errors. Otherwise, just return the return code.
267 if self.engine == 'slimerjs':
267 if self.engine == 'slimerjs':
268 stdout = bytes_to_str(self.stdout)
268 stdout = bytes_to_str(self.stdout)
269 if self.display_slimer_output:
269 if self.display_slimer_output:
270 print(stdout)
270 print(stdout)
271 if ret != 0:
271 if ret != 0:
272 # This could still happen e.g. if it's stopped by SIGINT
272 # This could still happen e.g. if it's stopped by SIGINT
273 return ret
273 return ret
274 return bool(self.slimer_failure.search(strip_ansi(stdout)))
274 return bool(self.slimer_failure.search(strip_ansi(stdout)))
275 else:
275 else:
276 return ret
276 return ret
277
277
278 def print_extra_info(self):
278 def print_extra_info(self):
279 print("Running tests with notebook directory %r" % self.nbdir.name)
279 print("Running tests with notebook directory %r" % self.nbdir.name)
280
280
281 @property
281 @property
282 def will_run(self):
282 def will_run(self):
283 return self.enabled and all(have[a] for a in self.requirements + [self.engine])
283 return self.enabled and all(have[a] for a in self.requirements + [self.engine])
284
284
285 def _init_server(self):
285 def _init_server(self):
286 "Start the notebook server in a separate process"
286 "Start the notebook server in a separate process"
287 self.server_command = command = [sys.executable,
287 self.server_command = command = [sys.executable,
288 '-m', 'IPython.html',
288 '-m', 'IPython.html',
289 '--no-browser',
289 '--no-browser',
290 '--ipython-dir', self.ipydir.name,
290 '--ipython-dir', self.ipydir.name,
291 '--notebook-dir', self.nbdir.name,
291 '--notebook-dir', self.nbdir.name,
292 ]
292 ]
293 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
293 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
294 # which run afoul of ipc's maximum path length.
294 # which run afoul of ipc's maximum path length.
295 if sys.platform.startswith('linux'):
295 if sys.platform.startswith('linux'):
296 command.append('--KernelManager.transport=ipc')
296 command.append('--KernelManager.transport=ipc')
297 self.stream_capturer = c = StreamCapturer()
297 self.stream_capturer = c = StreamCapturer()
298 c.start()
298 c.start()
299 self.server = subprocess.Popen(command, stdout=c.writefd, stderr=subprocess.STDOUT)
299 self.server = subprocess.Popen(command, stdout=c.writefd, stderr=subprocess.STDOUT)
300 self.server_info_file = os.path.join(self.ipydir.name,
300 self.server_info_file = os.path.join(self.ipydir.name,
301 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
301 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
302 )
302 )
303 self._wait_for_server()
303 self._wait_for_server()
304
304
305 def _wait_for_server(self):
305 def _wait_for_server(self):
306 """Wait 30 seconds for the notebook server to start"""
306 """Wait 30 seconds for the notebook server to start"""
307 for i in range(300):
307 for i in range(300):
308 if self.server.poll() is not None:
308 if self.server.poll() is not None:
309 return self._failed_to_start()
309 return self._failed_to_start()
310 if os.path.exists(self.server_info_file):
310 if os.path.exists(self.server_info_file):
311 try:
311 try:
312 self._load_server_info()
312 self._load_server_info()
313 except ValueError:
313 except ValueError:
314 # If the server is halfway through writing the file, we may
314 # If the server is halfway through writing the file, we may
315 # get invalid JSON; it should be ready next iteration.
315 # get invalid JSON; it should be ready next iteration.
316 pass
316 pass
317 else:
317 else:
318 return
318 return
319 time.sleep(0.1)
319 time.sleep(0.1)
320 print("Notebook server-info file never arrived: %s" % self.server_info_file,
320 print("Notebook server-info file never arrived: %s" % self.server_info_file,
321 file=sys.stderr
321 file=sys.stderr
322 )
322 )
323
323
324 def _failed_to_start(self):
324 def _failed_to_start(self):
325 """Notebook server exited prematurely"""
325 """Notebook server exited prematurely"""
326 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
326 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
327 print("Notebook failed to start: ", file=sys.stderr)
327 print("Notebook failed to start: ", file=sys.stderr)
328 print(self.server_command)
328 print(self.server_command)
329 print(captured, file=sys.stderr)
329 print(captured, file=sys.stderr)
330
330
331 def _load_server_info(self):
331 def _load_server_info(self):
332 """Notebook server started, load connection info from JSON"""
332 """Notebook server started, load connection info from JSON"""
333 with open(self.server_info_file) as f:
333 with open(self.server_info_file) as f:
334 info = json.load(f)
334 info = json.load(f)
335 self.server_port = info['port']
335 self.server_port = info['port']
336
336
337 def cleanup(self):
337 def cleanup(self):
338 try:
338 try:
339 self.server.terminate()
339 self.server.terminate()
340 except OSError:
340 except OSError:
341 # already dead
341 # already dead
342 pass
342 pass
343 # wait 10s for the server to shutdown
343 # wait 10s for the server to shutdown
344 try:
344 try:
345 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
345 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
346 except TimeoutExpired:
346 except TimeoutExpired:
347 # server didn't terminate, kill it
347 # server didn't terminate, kill it
348 try:
348 try:
349 print("Failed to terminate notebook server, killing it.",
349 print("Failed to terminate notebook server, killing it.",
350 file=sys.stderr
350 file=sys.stderr
351 )
351 )
352 self.server.kill()
352 self.server.kill()
353 except OSError:
353 except OSError:
354 # already dead
354 # already dead
355 pass
355 pass
356 # wait another 10s
356 # wait another 10s
357 try:
357 try:
358 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
358 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
359 except TimeoutExpired:
359 except TimeoutExpired:
360 print("Notebook server still running (%s)" % self.server_info_file,
360 print("Notebook server still running (%s)" % self.server_info_file,
361 file=sys.stderr
361 file=sys.stderr
362 )
362 )
363
363
364 self.stream_capturer.halt()
364 self.stream_capturer.halt()
365 TestController.cleanup(self)
365 TestController.cleanup(self)
366
366
367
367
368 def prepare_controllers(options):
368 def prepare_controllers(options):
369 """Returns two lists of TestController instances, those to run, and those
369 """Returns two lists of TestController instances, those to run, and those
370 not to run."""
370 not to run."""
371 testgroups = options.testgroups
371 testgroups = options.testgroups
372 if testgroups:
372 if testgroups:
373 if 'js' in testgroups:
373 if 'js' in testgroups:
374 js_testgroups = all_js_groups()
374 js_testgroups = all_js_groups()
375 else:
375 else:
376 js_testgroups = [g for g in testgroups if g.startswith(js_prefix)]
376 js_testgroups = [g for g in testgroups if g.startswith(js_prefix)]
377
377
378 py_testgroups = [g for g in testgroups if g not in ['js'] + js_testgroups]
378 py_testgroups = [g for g in testgroups if g not in ['js'] + js_testgroups]
379 else:
379 else:
380 py_testgroups = py_test_group_names
380 py_testgroups = py_test_group_names
381 if not options.all:
381 if not options.all:
382 js_testgroups = []
382 js_testgroups = []
383 test_sections['parallel'].enabled = False
383 test_sections['parallel'].enabled = False
384 else:
384 else:
385 js_testgroups = all_js_groups()
385 js_testgroups = all_js_groups()
386
386
387 engine = 'slimerjs' if options.slimerjs else 'phantomjs'
387 engine = 'slimerjs' if options.slimerjs else 'phantomjs'
388 c_js = [JSController(name, engine=engine) for name in js_testgroups]
388 c_js = [JSController(name, engine=engine) for name in js_testgroups]
389 c_py = [PyTestController(name, options) for name in py_testgroups]
389 c_py = [PyTestController(name, options) for name in py_testgroups]
390
390
391 controllers = c_py + c_js
391 controllers = c_py + c_js
392 to_run = [c for c in controllers if c.will_run]
392 to_run = [c for c in controllers if c.will_run]
393 not_run = [c for c in controllers if not c.will_run]
393 not_run = [c for c in controllers if not c.will_run]
394 return to_run, not_run
394 return to_run, not_run
395
395
396 def do_run(controller, buffer_output=True):
396 def do_run(controller, buffer_output=True):
397 """Setup and run a test controller.
397 """Setup and run a test controller.
398
398
399 If buffer_output is True, no output is displayed, to avoid it appearing
399 If buffer_output is True, no output is displayed, to avoid it appearing
400 interleaved. In this case, the caller is responsible for displaying test
400 interleaved. In this case, the caller is responsible for displaying test
401 output on failure.
401 output on failure.
402
402
403 Returns
403 Returns
404 -------
404 -------
405 controller : TestController
405 controller : TestController
406 The same controller as passed in, as a convenience for using map() type
406 The same controller as passed in, as a convenience for using map() type
407 APIs.
407 APIs.
408 exitcode : int
408 exitcode : int
409 The exit code of the test subprocess. Non-zero indicates failure.
409 The exit code of the test subprocess. Non-zero indicates failure.
410 """
410 """
411 try:
411 try:
412 try:
412 try:
413 controller.setup()
413 controller.setup()
414 if not buffer_output:
414 if not buffer_output:
415 controller.print_extra_info()
415 controller.print_extra_info()
416 controller.launch(buffer_output=buffer_output)
416 controller.launch(buffer_output=buffer_output)
417 except Exception:
417 except Exception:
418 import traceback
418 import traceback
419 traceback.print_exc()
419 traceback.print_exc()
420 return controller, 1 # signal failure
420 return controller, 1 # signal failure
421
421
422 exitcode = controller.wait()
422 exitcode = controller.wait()
423 return controller, exitcode
423 return controller, exitcode
424
424
425 except KeyboardInterrupt:
425 except KeyboardInterrupt:
426 return controller, -signal.SIGINT
426 return controller, -signal.SIGINT
427 finally:
427 finally:
428 controller.cleanup()
428 controller.cleanup()
429
429
430 def report():
430 def report():
431 """Return a string with a summary report of test-related variables."""
431 """Return a string with a summary report of test-related variables."""
432 inf = get_sys_info()
432 inf = get_sys_info()
433 out = []
433 out = []
434 def _add(name, value):
434 def _add(name, value):
435 out.append((name, value))
435 out.append((name, value))
436
436
437 _add('IPython version', inf['ipython_version'])
437 _add('IPython version', inf['ipython_version'])
438 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
438 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
439 _add('IPython package', compress_user(inf['ipython_path']))
439 _add('IPython package', compress_user(inf['ipython_path']))
440 _add('Python version', inf['sys_version'].replace('\n',''))
440 _add('Python version', inf['sys_version'].replace('\n',''))
441 _add('sys.executable', compress_user(inf['sys_executable']))
441 _add('sys.executable', compress_user(inf['sys_executable']))
442 _add('Platform', inf['platform'])
442 _add('Platform', inf['platform'])
443
443
444 width = max(len(n) for (n,v) in out)
444 width = max(len(n) for (n,v) in out)
445 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
445 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
446
446
447 avail = []
447 avail = []
448 not_avail = []
448 not_avail = []
449
449
450 for k, is_avail in have.items():
450 for k, is_avail in have.items():
451 if is_avail:
451 if is_avail:
452 avail.append(k)
452 avail.append(k)
453 else:
453 else:
454 not_avail.append(k)
454 not_avail.append(k)
455
455
456 if avail:
456 if avail:
457 out.append('\nTools and libraries available at test time:\n')
457 out.append('\nTools and libraries available at test time:\n')
458 avail.sort()
458 avail.sort()
459 out.append(' ' + ' '.join(avail)+'\n')
459 out.append(' ' + ' '.join(avail)+'\n')
460
460
461 if not_avail:
461 if not_avail:
462 out.append('\nTools and libraries NOT available at test time:\n')
462 out.append('\nTools and libraries NOT available at test time:\n')
463 not_avail.sort()
463 not_avail.sort()
464 out.append(' ' + ' '.join(not_avail)+'\n')
464 out.append(' ' + ' '.join(not_avail)+'\n')
465
465
466 return ''.join(out)
466 return ''.join(out)
467
467
468 def run_iptestall(options):
468 def run_iptestall(options):
469 """Run the entire IPython test suite by calling nose and trial.
469 """Run the entire IPython test suite by calling nose and trial.
470
470
471 This function constructs :class:`IPTester` instances for all IPython
471 This function constructs :class:`IPTester` instances for all IPython
472 modules and package and then runs each of them. This causes the modules
472 modules and package and then runs each of them. This causes the modules
473 and packages of IPython to be tested each in their own subprocess using
473 and packages of IPython to be tested each in their own subprocess using
474 nose.
474 nose.
475
475
476 Parameters
476 Parameters
477 ----------
477 ----------
478
478
479 All parameters are passed as attributes of the options object.
479 All parameters are passed as attributes of the options object.
480
480
481 testgroups : list of str
481 testgroups : list of str
482 Run only these sections of the test suite. If empty, run all the available
482 Run only these sections of the test suite. If empty, run all the available
483 sections.
483 sections.
484
484
485 fast : int or None
485 fast : int or None
486 Run the test suite in parallel, using n simultaneous processes. If None
486 Run the test suite in parallel, using n simultaneous processes. If None
487 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
487 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
488
488
489 inc_slow : bool
489 inc_slow : bool
490 Include slow tests, like IPython.parallel. By default, these tests aren't
490 Include slow tests, like IPython.parallel. By default, these tests aren't
491 run.
491 run.
492
492
493 slimerjs : bool
493 slimerjs : bool
494 Use slimerjs if it's installed instead of phantomjs for casperjs tests.
494 Use slimerjs if it's installed instead of phantomjs for casperjs tests.
495
495
496 xunit : bool
496 xunit : bool
497 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
497 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
498
498
499 coverage : bool or str
499 coverage : bool or str
500 Measure code coverage from tests. True will store the raw coverage data,
500 Measure code coverage from tests. True will store the raw coverage data,
501 or pass 'html' or 'xml' to get reports.
501 or pass 'html' or 'xml' to get reports.
502
502
503 extra_args : list
503 extra_args : list
504 Extra arguments to pass to the test subprocesses, e.g. '-v'
504 Extra arguments to pass to the test subprocesses, e.g. '-v'
505 """
505 """
506 to_run, not_run = prepare_controllers(options)
506 to_run, not_run = prepare_controllers(options)
507
507
508 def justify(ltext, rtext, width=70, fill='-'):
508 def justify(ltext, rtext, width=70, fill='-'):
509 ltext += ' '
509 ltext += ' '
510 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
510 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
511 return ltext + rtext
511 return ltext + rtext
512
512
513 # Run all test runners, tracking execution time
513 # Run all test runners, tracking execution time
514 failed = []
514 failed = []
515 t_start = time.time()
515 t_start = time.time()
516
516
517 print()
517 print()
518 if options.fast == 1:
518 if options.fast == 1:
519 # This actually means sequential, i.e. with 1 job
519 # This actually means sequential, i.e. with 1 job
520 for controller in to_run:
520 for controller in to_run:
521 print('Test group:', controller.section)
521 print('Test group:', controller.section)
522 sys.stdout.flush() # Show in correct order when output is piped
522 sys.stdout.flush() # Show in correct order when output is piped
523 controller, res = do_run(controller, buffer_output=False)
523 controller, res = do_run(controller, buffer_output=False)
524 if res:
524 if res:
525 failed.append(controller)
525 failed.append(controller)
526 if res == -signal.SIGINT:
526 if res == -signal.SIGINT:
527 print("Interrupted")
527 print("Interrupted")
528 break
528 break
529 print()
529 print()
530
530
531 else:
531 else:
532 # Run tests concurrently
532 # Run tests concurrently
533 try:
533 try:
534 pool = multiprocessing.pool.ThreadPool(options.fast)
534 pool = multiprocessing.pool.ThreadPool(options.fast)
535 for (controller, res) in pool.imap_unordered(do_run, to_run):
535 for (controller, res) in pool.imap_unordered(do_run, to_run):
536 res_string = 'OK' if res == 0 else 'FAILED'
536 res_string = 'OK' if res == 0 else 'FAILED'
537 print(justify('Test group: ' + controller.section, res_string))
537 print(justify('Test group: ' + controller.section, res_string))
538 if res:
538 if res:
539 controller.print_extra_info()
539 controller.print_extra_info()
540 print(bytes_to_str(controller.stdout))
540 print(bytes_to_str(controller.stdout))
541 failed.append(controller)
541 failed.append(controller)
542 if res == -signal.SIGINT:
542 if res == -signal.SIGINT:
543 print("Interrupted")
543 print("Interrupted")
544 break
544 break
545 except KeyboardInterrupt:
545 except KeyboardInterrupt:
546 return
546 return
547
547
548 for controller in not_run:
548 for controller in not_run:
549 print(justify('Test group: ' + controller.section, 'NOT RUN'))
549 print(justify('Test group: ' + controller.section, 'NOT RUN'))
550
550
551 t_end = time.time()
551 t_end = time.time()
552 t_tests = t_end - t_start
552 t_tests = t_end - t_start
553 nrunners = len(to_run)
553 nrunners = len(to_run)
554 nfail = len(failed)
554 nfail = len(failed)
555 # summarize results
555 # summarize results
556 print('_'*70)
556 print('_'*70)
557 print('Test suite completed for system with the following information:')
557 print('Test suite completed for system with the following information:')
558 print(report())
558 print(report())
559 took = "Took %.3fs." % t_tests
559 took = "Took %.3fs." % t_tests
560 print('Status: ', end='')
560 print('Status: ', end='')
561 if not failed:
561 if not failed:
562 print('OK (%d test groups).' % nrunners, took)
562 print('OK (%d test groups).' % nrunners, took)
563 else:
563 else:
564 # If anything went wrong, point out what command to rerun manually to
564 # If anything went wrong, point out what command to rerun manually to
565 # see the actual errors and individual summary
565 # see the actual errors and individual summary
566 failed_sections = [c.section for c in failed]
566 failed_sections = [c.section for c in failed]
567 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
567 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
568 nrunners, ', '.join(failed_sections)), took)
568 nrunners, ', '.join(failed_sections)), took)
569 print()
569 print()
570 print('You may wish to rerun these, with:')
570 print('You may wish to rerun these, with:')
571 print(' iptest', *failed_sections)
571 print(' iptest', *failed_sections)
572 print()
572 print()
573
573
574 if options.coverage:
574 if options.coverage:
575 from coverage import coverage
575 from coverage import coverage
576 cov = coverage(data_file='.coverage')
576 cov = coverage(data_file='.coverage')
577 cov.combine()
577 cov.combine()
578 cov.save()
578 cov.save()
579
579
580 # Coverage HTML report
580 # Coverage HTML report
581 if options.coverage == 'html':
581 if options.coverage == 'html':
582 html_dir = 'ipy_htmlcov'
582 html_dir = 'ipy_htmlcov'
583 shutil.rmtree(html_dir, ignore_errors=True)
583 shutil.rmtree(html_dir, ignore_errors=True)
584 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
584 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
585 sys.stdout.flush()
585 sys.stdout.flush()
586
586
587 # Custom HTML reporter to clean up module names.
587 # Custom HTML reporter to clean up module names.
588 from coverage.html import HtmlReporter
588 from coverage.html import HtmlReporter
589 class CustomHtmlReporter(HtmlReporter):
589 class CustomHtmlReporter(HtmlReporter):
590 def find_code_units(self, morfs):
590 def find_code_units(self, morfs):
591 super(CustomHtmlReporter, self).find_code_units(morfs)
591 super(CustomHtmlReporter, self).find_code_units(morfs)
592 for cu in self.code_units:
592 for cu in self.code_units:
593 nameparts = cu.name.split(os.sep)
593 nameparts = cu.name.split(os.sep)
594 if 'IPython' not in nameparts:
594 if 'IPython' not in nameparts:
595 continue
595 continue
596 ix = nameparts.index('IPython')
596 ix = nameparts.index('IPython')
597 cu.name = '.'.join(nameparts[ix:])
597 cu.name = '.'.join(nameparts[ix:])
598
598
599 # Reimplement the html_report method with our custom reporter
599 # Reimplement the html_report method with our custom reporter
600 cov._harvest_data()
600 cov._harvest_data()
601 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
601 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
602 html_title='IPython test coverage',
602 html_title='IPython test coverage',
603 )
603 )
604 reporter = CustomHtmlReporter(cov, cov.config)
604 reporter = CustomHtmlReporter(cov, cov.config)
605 reporter.report(None)
605 reporter.report(None)
606 print('done.')
606 print('done.')
607
607
608 # Coverage XML report
608 # Coverage XML report
609 elif options.coverage == 'xml':
609 elif options.coverage == 'xml':
610 cov.xml_report(outfile='ipy_coverage.xml')
610 cov.xml_report(outfile='ipy_coverage.xml')
611
611
612 if failed:
612 if failed:
613 # Ensure that our exit code indicates failure
613 # Ensure that our exit code indicates failure
614 sys.exit(1)
614 sys.exit(1)
615
615
616 argparser = argparse.ArgumentParser(description='Run IPython test suite')
616 argparser = argparse.ArgumentParser(description='Run IPython test suite')
617 argparser.add_argument('testgroups', nargs='*',
617 argparser.add_argument('testgroups', nargs='*',
618 help='Run specified groups of tests. If omitted, run '
618 help='Run specified groups of tests. If omitted, run '
619 'all tests.')
619 'all tests.')
620 argparser.add_argument('--all', action='store_true',
620 argparser.add_argument('--all', action='store_true',
621 help='Include slow tests not run by default.')
621 help='Include slow tests not run by default.')
622 argparser.add_argument('--slimerjs', action='store_true',
622 argparser.add_argument('--slimerjs', action='store_true',
623 help="Use slimerjs if it's installed instead of phantomjs for casperjs tests.")
623 help="Use slimerjs if it's installed instead of phantomjs for casperjs tests.")
624 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
624 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
625 help='Run test sections in parallel. This starts as many '
625 help='Run test sections in parallel. This starts as many '
626 'processes as you have cores, or you can specify a number.')
626 'processes as you have cores, or you can specify a number.')
627 argparser.add_argument('--xunit', action='store_true',
627 argparser.add_argument('--xunit', action='store_true',
628 help='Produce Xunit XML results')
628 help='Produce Xunit XML results')
629 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
629 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
630 help="Measure test coverage. Specify 'html' or "
630 help="Measure test coverage. Specify 'html' or "
631 "'xml' to get reports.")
631 "'xml' to get reports.")
632 argparser.add_argument('--subproc-streams', default='capture',
632 argparser.add_argument('--subproc-streams', default='capture',
633 help="What to do with stdout/stderr from subprocesses. "
633 help="What to do with stdout/stderr from subprocesses. "
634 "'capture' (default), 'show' and 'discard' are the options.")
634 "'capture' (default), 'show' and 'discard' are the options.")
635
635
636 def default_options():
636 def default_options():
637 """Get an argparse Namespace object with the default arguments, to pass to
637 """Get an argparse Namespace object with the default arguments, to pass to
638 :func:`run_iptestall`.
638 :func:`run_iptestall`.
639 """
639 """
640 options = argparser.parse_args([])
640 options = argparser.parse_args([])
641 options.extra_args = []
641 options.extra_args = []
642 return options
642 return options
643
643
644 def main():
644 def main():
645 # iptest doesn't work correctly if the working directory is the
645 # iptest doesn't work correctly if the working directory is the
646 # root of the IPython source tree. Tell the user to avoid
646 # root of the IPython source tree. Tell the user to avoid
647 # frustration.
647 # frustration.
648 if os.path.exists(os.path.join(os.getcwd(),
648 if os.path.exists(os.path.join(os.getcwd(),
649 'IPython', 'testing', '__main__.py')):
649 'IPython', 'testing', '__main__.py')):
650 print("Don't run iptest from the IPython source directory",
650 print("Don't run iptest from the IPython source directory",
651 file=sys.stderr)
651 file=sys.stderr)
652 sys.exit(1)
652 sys.exit(1)
653 # Arguments after -- should be passed through to nose. Argparse treats
653 # Arguments after -- should be passed through to nose. Argparse treats
654 # everything after -- as regular positional arguments, so we separate them
654 # everything after -- as regular positional arguments, so we separate them
655 # first.
655 # first.
656 try:
656 try:
657 ix = sys.argv.index('--')
657 ix = sys.argv.index('--')
658 except ValueError:
658 except ValueError:
659 to_parse = sys.argv[1:]
659 to_parse = sys.argv[1:]
660 extra_args = []
660 extra_args = []
661 else:
661 else:
662 to_parse = sys.argv[1:ix]
662 to_parse = sys.argv[1:ix]
663 extra_args = sys.argv[ix+1:]
663 extra_args = sys.argv[ix+1:]
664
664
665 options = argparser.parse_args(to_parse)
665 options = argparser.parse_args(to_parse)
666 options.extra_args = extra_args
666 options.extra_args = extra_args
667
667
668 run_iptestall(options)
668 run_iptestall(options)
669
669
670
670
671 if __name__ == '__main__':
671 if __name__ == '__main__':
672 main()
672 main()
@@ -1,773 +1,784 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 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Copyright (C) 2008-2011 The IPython Development Team
12 # Copyright (C) 2008-2011 The IPython Development Team
13 #
13 #
14 # Distributed under the terms of the BSD License. The full license is in
14 # Distributed under the terms of the BSD License. The full license is in
15 # the file COPYING, distributed as part of this software.
15 # the file COPYING, distributed as part of this software.
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Imports
19 # Imports
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 import os
22 import os
23 import re
23 import re
24 import sys
24 import sys
25 import textwrap
25 import textwrap
26 from string import Formatter
26 from string import Formatter
27
27
28 from IPython.external.path import path
28 from IPython.external.path import path
29 from IPython.testing.skipdoctest import skip_doctest_py3, skip_doctest
29 from IPython.testing.skipdoctest import skip_doctest_py3, skip_doctest
30 from IPython.utils import py3compat
30 from IPython.utils import py3compat
31
31
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33 # Declarations
33 # Declarations
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35
35
36 # datetime.strftime date format for ipython
36 # datetime.strftime date format for ipython
37 if sys.platform == 'win32':
37 if sys.platform == 'win32':
38 date_format = "%B %d, %Y"
38 date_format = "%B %d, %Y"
39 else:
39 else:
40 date_format = "%B %-d, %Y"
40 date_format = "%B %-d, %Y"
41
41
42
42
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44 # Code
44 # Code
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46
46
47 class LSString(str):
47 class LSString(str):
48 """String derivative with a special access attributes.
48 """String derivative with a special access attributes.
49
49
50 These are normal strings, but with the special attributes:
50 These are normal strings, but with the special attributes:
51
51
52 .l (or .list) : value as list (split on newlines).
52 .l (or .list) : value as list (split on newlines).
53 .n (or .nlstr): original value (the string itself).
53 .n (or .nlstr): original value (the string itself).
54 .s (or .spstr): value as whitespace-separated string.
54 .s (or .spstr): value as whitespace-separated string.
55 .p (or .paths): list of path objects
55 .p (or .paths): list of path objects
56
56
57 Any values which require transformations are computed only once and
57 Any values which require transformations are computed only once and
58 cached.
58 cached.
59
59
60 Such strings are very useful to efficiently interact with the shell, which
60 Such strings are very useful to efficiently interact with the shell, which
61 typically only understands whitespace-separated options for commands."""
61 typically only understands whitespace-separated options for commands."""
62
62
63 def get_list(self):
63 def get_list(self):
64 try:
64 try:
65 return self.__list
65 return self.__list
66 except AttributeError:
66 except AttributeError:
67 self.__list = self.split('\n')
67 self.__list = self.split('\n')
68 return self.__list
68 return self.__list
69
69
70 l = list = property(get_list)
70 l = list = property(get_list)
71
71
72 def get_spstr(self):
72 def get_spstr(self):
73 try:
73 try:
74 return self.__spstr
74 return self.__spstr
75 except AttributeError:
75 except AttributeError:
76 self.__spstr = self.replace('\n',' ')
76 self.__spstr = self.replace('\n',' ')
77 return self.__spstr
77 return self.__spstr
78
78
79 s = spstr = property(get_spstr)
79 s = spstr = property(get_spstr)
80
80
81 def get_nlstr(self):
81 def get_nlstr(self):
82 return self
82 return self
83
83
84 n = nlstr = property(get_nlstr)
84 n = nlstr = property(get_nlstr)
85
85
86 def get_paths(self):
86 def get_paths(self):
87 try:
87 try:
88 return self.__paths
88 return self.__paths
89 except AttributeError:
89 except AttributeError:
90 self.__paths = [path(p) for p in self.split('\n') if os.path.exists(p)]
90 self.__paths = [path(p) for p in self.split('\n') if os.path.exists(p)]
91 return self.__paths
91 return self.__paths
92
92
93 p = paths = property(get_paths)
93 p = paths = property(get_paths)
94
94
95 # FIXME: We need to reimplement type specific displayhook and then add this
95 # FIXME: We need to reimplement type specific displayhook and then add this
96 # back as a custom printer. This should also be moved outside utils into the
96 # back as a custom printer. This should also be moved outside utils into the
97 # core.
97 # core.
98
98
99 # def print_lsstring(arg):
99 # def print_lsstring(arg):
100 # """ Prettier (non-repr-like) and more informative printer for LSString """
100 # """ Prettier (non-repr-like) and more informative printer for LSString """
101 # print "LSString (.p, .n, .l, .s available). Value:"
101 # print "LSString (.p, .n, .l, .s available). Value:"
102 # print arg
102 # print arg
103 #
103 #
104 #
104 #
105 # print_lsstring = result_display.when_type(LSString)(print_lsstring)
105 # print_lsstring = result_display.when_type(LSString)(print_lsstring)
106
106
107
107
108 class SList(list):
108 class SList(list):
109 """List derivative with a special access attributes.
109 """List derivative with a special access attributes.
110
110
111 These are normal lists, but with the special attributes:
111 These are normal lists, but with the special attributes:
112
112
113 * .l (or .list) : value as list (the list itself).
113 * .l (or .list) : value as list (the list itself).
114 * .n (or .nlstr): value as a string, joined on newlines.
114 * .n (or .nlstr): value as a string, joined on newlines.
115 * .s (or .spstr): value as a string, joined on spaces.
115 * .s (or .spstr): value as a string, joined on spaces.
116 * .p (or .paths): list of path objects
116 * .p (or .paths): list of path objects
117
117
118 Any values which require transformations are computed only once and
118 Any values which require transformations are computed only once and
119 cached."""
119 cached."""
120
120
121 def get_list(self):
121 def get_list(self):
122 return self
122 return self
123
123
124 l = list = property(get_list)
124 l = list = property(get_list)
125
125
126 def get_spstr(self):
126 def get_spstr(self):
127 try:
127 try:
128 return self.__spstr
128 return self.__spstr
129 except AttributeError:
129 except AttributeError:
130 self.__spstr = ' '.join(self)
130 self.__spstr = ' '.join(self)
131 return self.__spstr
131 return self.__spstr
132
132
133 s = spstr = property(get_spstr)
133 s = spstr = property(get_spstr)
134
134
135 def get_nlstr(self):
135 def get_nlstr(self):
136 try:
136 try:
137 return self.__nlstr
137 return self.__nlstr
138 except AttributeError:
138 except AttributeError:
139 self.__nlstr = '\n'.join(self)
139 self.__nlstr = '\n'.join(self)
140 return self.__nlstr
140 return self.__nlstr
141
141
142 n = nlstr = property(get_nlstr)
142 n = nlstr = property(get_nlstr)
143
143
144 def get_paths(self):
144 def get_paths(self):
145 try:
145 try:
146 return self.__paths
146 return self.__paths
147 except AttributeError:
147 except AttributeError:
148 self.__paths = [path(p) for p in self if os.path.exists(p)]
148 self.__paths = [path(p) for p in self if os.path.exists(p)]
149 return self.__paths
149 return self.__paths
150
150
151 p = paths = property(get_paths)
151 p = paths = property(get_paths)
152
152
153 def grep(self, pattern, prune = False, field = None):
153 def grep(self, pattern, prune = False, field = None):
154 """ Return all strings matching 'pattern' (a regex or callable)
154 """ Return all strings matching 'pattern' (a regex or callable)
155
155
156 This is case-insensitive. If prune is true, return all items
156 This is case-insensitive. If prune is true, return all items
157 NOT matching the pattern.
157 NOT matching the pattern.
158
158
159 If field is specified, the match must occur in the specified
159 If field is specified, the match must occur in the specified
160 whitespace-separated field.
160 whitespace-separated field.
161
161
162 Examples::
162 Examples::
163
163
164 a.grep( lambda x: x.startswith('C') )
164 a.grep( lambda x: x.startswith('C') )
165 a.grep('Cha.*log', prune=1)
165 a.grep('Cha.*log', prune=1)
166 a.grep('chm', field=-1)
166 a.grep('chm', field=-1)
167 """
167 """
168
168
169 def match_target(s):
169 def match_target(s):
170 if field is None:
170 if field is None:
171 return s
171 return s
172 parts = s.split()
172 parts = s.split()
173 try:
173 try:
174 tgt = parts[field]
174 tgt = parts[field]
175 return tgt
175 return tgt
176 except IndexError:
176 except IndexError:
177 return ""
177 return ""
178
178
179 if isinstance(pattern, py3compat.string_types):
179 if isinstance(pattern, py3compat.string_types):
180 pred = lambda x : re.search(pattern, x, re.IGNORECASE)
180 pred = lambda x : re.search(pattern, x, re.IGNORECASE)
181 else:
181 else:
182 pred = pattern
182 pred = pattern
183 if not prune:
183 if not prune:
184 return SList([el for el in self if pred(match_target(el))])
184 return SList([el for el in self if pred(match_target(el))])
185 else:
185 else:
186 return SList([el for el in self if not pred(match_target(el))])
186 return SList([el for el in self if not pred(match_target(el))])
187
187
188 def fields(self, *fields):
188 def fields(self, *fields):
189 """ Collect whitespace-separated fields from string list
189 """ Collect whitespace-separated fields from string list
190
190
191 Allows quick awk-like usage of string lists.
191 Allows quick awk-like usage of string lists.
192
192
193 Example data (in var a, created by 'a = !ls -l')::
193 Example data (in var a, created by 'a = !ls -l')::
194
194
195 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
195 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
196 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
196 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
197
197
198 * ``a.fields(0)`` is ``['-rwxrwxrwx', 'drwxrwxrwx+']``
198 * ``a.fields(0)`` is ``['-rwxrwxrwx', 'drwxrwxrwx+']``
199 * ``a.fields(1,0)`` is ``['1 -rwxrwxrwx', '6 drwxrwxrwx+']``
199 * ``a.fields(1,0)`` is ``['1 -rwxrwxrwx', '6 drwxrwxrwx+']``
200 (note the joining by space).
200 (note the joining by space).
201 * ``a.fields(-1)`` is ``['ChangeLog', 'IPython']``
201 * ``a.fields(-1)`` is ``['ChangeLog', 'IPython']``
202
202
203 IndexErrors are ignored.
203 IndexErrors are ignored.
204
204
205 Without args, fields() just split()'s the strings.
205 Without args, fields() just split()'s the strings.
206 """
206 """
207 if len(fields) == 0:
207 if len(fields) == 0:
208 return [el.split() for el in self]
208 return [el.split() for el in self]
209
209
210 res = SList()
210 res = SList()
211 for el in [f.split() for f in self]:
211 for el in [f.split() for f in self]:
212 lineparts = []
212 lineparts = []
213
213
214 for fd in fields:
214 for fd in fields:
215 try:
215 try:
216 lineparts.append(el[fd])
216 lineparts.append(el[fd])
217 except IndexError:
217 except IndexError:
218 pass
218 pass
219 if lineparts:
219 if lineparts:
220 res.append(" ".join(lineparts))
220 res.append(" ".join(lineparts))
221
221
222 return res
222 return res
223
223
224 def sort(self,field= None, nums = False):
224 def sort(self,field= None, nums = False):
225 """ sort by specified fields (see fields())
225 """ sort by specified fields (see fields())
226
226
227 Example::
227 Example::
228
228
229 a.sort(1, nums = True)
229 a.sort(1, nums = True)
230
230
231 Sorts a by second field, in numerical order (so that 21 > 3)
231 Sorts a by second field, in numerical order (so that 21 > 3)
232
232
233 """
233 """
234
234
235 #decorate, sort, undecorate
235 #decorate, sort, undecorate
236 if field is not None:
236 if field is not None:
237 dsu = [[SList([line]).fields(field), line] for line in self]
237 dsu = [[SList([line]).fields(field), line] for line in self]
238 else:
238 else:
239 dsu = [[line, line] for line in self]
239 dsu = [[line, line] for line in self]
240 if nums:
240 if nums:
241 for i in range(len(dsu)):
241 for i in range(len(dsu)):
242 numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
242 numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
243 try:
243 try:
244 n = int(numstr)
244 n = int(numstr)
245 except ValueError:
245 except ValueError:
246 n = 0;
246 n = 0;
247 dsu[i][0] = n
247 dsu[i][0] = n
248
248
249
249
250 dsu.sort()
250 dsu.sort()
251 return SList([t[1] for t in dsu])
251 return SList([t[1] for t in dsu])
252
252
253
253
254 # FIXME: We need to reimplement type specific displayhook and then add this
254 # FIXME: We need to reimplement type specific displayhook and then add this
255 # back as a custom printer. This should also be moved outside utils into the
255 # back as a custom printer. This should also be moved outside utils into the
256 # core.
256 # core.
257
257
258 # def print_slist(arg):
258 # def print_slist(arg):
259 # """ Prettier (non-repr-like) and more informative printer for SList """
259 # """ Prettier (non-repr-like) and more informative printer for SList """
260 # print "SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):"
260 # print "SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):"
261 # if hasattr(arg, 'hideonce') and arg.hideonce:
261 # if hasattr(arg, 'hideonce') and arg.hideonce:
262 # arg.hideonce = False
262 # arg.hideonce = False
263 # return
263 # return
264 #
264 #
265 # nlprint(arg) # This was a nested list printer, now removed.
265 # nlprint(arg) # This was a nested list printer, now removed.
266 #
266 #
267 # print_slist = result_display.when_type(SList)(print_slist)
267 # print_slist = result_display.when_type(SList)(print_slist)
268
268
269
269
270 def indent(instr,nspaces=4, ntabs=0, flatten=False):
270 def indent(instr,nspaces=4, ntabs=0, flatten=False):
271 """Indent a string a given number of spaces or tabstops.
271 """Indent a string a given number of spaces or tabstops.
272
272
273 indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces.
273 indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces.
274
274
275 Parameters
275 Parameters
276 ----------
276 ----------
277
277
278 instr : basestring
278 instr : basestring
279 The string to be indented.
279 The string to be indented.
280 nspaces : int (default: 4)
280 nspaces : int (default: 4)
281 The number of spaces to be indented.
281 The number of spaces to be indented.
282 ntabs : int (default: 0)
282 ntabs : int (default: 0)
283 The number of tabs to be indented.
283 The number of tabs to be indented.
284 flatten : bool (default: False)
284 flatten : bool (default: False)
285 Whether to scrub existing indentation. If True, all lines will be
285 Whether to scrub existing indentation. If True, all lines will be
286 aligned to the same indentation. If False, existing indentation will
286 aligned to the same indentation. If False, existing indentation will
287 be strictly increased.
287 be strictly increased.
288
288
289 Returns
289 Returns
290 -------
290 -------
291
291
292 str|unicode : string indented by ntabs and nspaces.
292 str|unicode : string indented by ntabs and nspaces.
293
293
294 """
294 """
295 if instr is None:
295 if instr is None:
296 return
296 return
297 ind = '\t'*ntabs+' '*nspaces
297 ind = '\t'*ntabs+' '*nspaces
298 if flatten:
298 if flatten:
299 pat = re.compile(r'^\s*', re.MULTILINE)
299 pat = re.compile(r'^\s*', re.MULTILINE)
300 else:
300 else:
301 pat = re.compile(r'^', re.MULTILINE)
301 pat = re.compile(r'^', re.MULTILINE)
302 outstr = re.sub(pat, ind, instr)
302 outstr = re.sub(pat, ind, instr)
303 if outstr.endswith(os.linesep+ind):
303 if outstr.endswith(os.linesep+ind):
304 return outstr[:-len(ind)]
304 return outstr[:-len(ind)]
305 else:
305 else:
306 return outstr
306 return outstr
307
307
308
308
309 def list_strings(arg):
309 def list_strings(arg):
310 """Always return a list of strings, given a string or list of strings
310 """Always return a list of strings, given a string or list of strings
311 as input.
311 as input.
312
312
313 Examples
313 Examples
314 --------
314 --------
315 ::
315 ::
316
316
317 In [7]: list_strings('A single string')
317 In [7]: list_strings('A single string')
318 Out[7]: ['A single string']
318 Out[7]: ['A single string']
319
319
320 In [8]: list_strings(['A single string in a list'])
320 In [8]: list_strings(['A single string in a list'])
321 Out[8]: ['A single string in a list']
321 Out[8]: ['A single string in a list']
322
322
323 In [9]: list_strings(['A','list','of','strings'])
323 In [9]: list_strings(['A','list','of','strings'])
324 Out[9]: ['A', 'list', 'of', 'strings']
324 Out[9]: ['A', 'list', 'of', 'strings']
325 """
325 """
326
326
327 if isinstance(arg, py3compat.string_types): return [arg]
327 if isinstance(arg, py3compat.string_types): return [arg]
328 else: return arg
328 else: return arg
329
329
330
330
331 def marquee(txt='',width=78,mark='*'):
331 def marquee(txt='',width=78,mark='*'):
332 """Return the input string centered in a 'marquee'.
332 """Return the input string centered in a 'marquee'.
333
333
334 Examples
334 Examples
335 --------
335 --------
336 ::
336 ::
337
337
338 In [16]: marquee('A test',40)
338 In [16]: marquee('A test',40)
339 Out[16]: '**************** A test ****************'
339 Out[16]: '**************** A test ****************'
340
340
341 In [17]: marquee('A test',40,'-')
341 In [17]: marquee('A test',40,'-')
342 Out[17]: '---------------- A test ----------------'
342 Out[17]: '---------------- A test ----------------'
343
343
344 In [18]: marquee('A test',40,' ')
344 In [18]: marquee('A test',40,' ')
345 Out[18]: ' A test '
345 Out[18]: ' A test '
346
346
347 """
347 """
348 if not txt:
348 if not txt:
349 return (mark*width)[:width]
349 return (mark*width)[:width]
350 nmark = (width-len(txt)-2)//len(mark)//2
350 nmark = (width-len(txt)-2)//len(mark)//2
351 if nmark < 0: nmark =0
351 if nmark < 0: nmark =0
352 marks = mark*nmark
352 marks = mark*nmark
353 return '%s %s %s' % (marks,txt,marks)
353 return '%s %s %s' % (marks,txt,marks)
354
354
355
355
356 ini_spaces_re = re.compile(r'^(\s+)')
356 ini_spaces_re = re.compile(r'^(\s+)')
357
357
358 def num_ini_spaces(strng):
358 def num_ini_spaces(strng):
359 """Return the number of initial spaces in a string"""
359 """Return the number of initial spaces in a string"""
360
360
361 ini_spaces = ini_spaces_re.match(strng)
361 ini_spaces = ini_spaces_re.match(strng)
362 if ini_spaces:
362 if ini_spaces:
363 return ini_spaces.end()
363 return ini_spaces.end()
364 else:
364 else:
365 return 0
365 return 0
366
366
367
367
368 def format_screen(strng):
368 def format_screen(strng):
369 """Format a string for screen printing.
369 """Format a string for screen printing.
370
370
371 This removes some latex-type format codes."""
371 This removes some latex-type format codes."""
372 # Paragraph continue
372 # Paragraph continue
373 par_re = re.compile(r'\\$',re.MULTILINE)
373 par_re = re.compile(r'\\$',re.MULTILINE)
374 strng = par_re.sub('',strng)
374 strng = par_re.sub('',strng)
375 return strng
375 return strng
376
376
377
377
378 def dedent(text):
378 def dedent(text):
379 """Equivalent of textwrap.dedent that ignores unindented first line.
379 """Equivalent of textwrap.dedent that ignores unindented first line.
380
380
381 This means it will still dedent strings like:
381 This means it will still dedent strings like:
382 '''foo
382 '''foo
383 is a bar
383 is a bar
384 '''
384 '''
385
385
386 For use in wrap_paragraphs.
386 For use in wrap_paragraphs.
387 """
387 """
388
388
389 if text.startswith('\n'):
389 if text.startswith('\n'):
390 # text starts with blank line, don't ignore the first line
390 # text starts with blank line, don't ignore the first line
391 return textwrap.dedent(text)
391 return textwrap.dedent(text)
392
392
393 # split first line
393 # split first line
394 splits = text.split('\n',1)
394 splits = text.split('\n',1)
395 if len(splits) == 1:
395 if len(splits) == 1:
396 # only one line
396 # only one line
397 return textwrap.dedent(text)
397 return textwrap.dedent(text)
398
398
399 first, rest = splits
399 first, rest = splits
400 # dedent everything but the first line
400 # dedent everything but the first line
401 rest = textwrap.dedent(rest)
401 rest = textwrap.dedent(rest)
402 return '\n'.join([first, rest])
402 return '\n'.join([first, rest])
403
403
404
404
405 def wrap_paragraphs(text, ncols=80):
405 def wrap_paragraphs(text, ncols=80):
406 """Wrap multiple paragraphs to fit a specified width.
406 """Wrap multiple paragraphs to fit a specified width.
407
407
408 This is equivalent to textwrap.wrap, but with support for multiple
408 This is equivalent to textwrap.wrap, but with support for multiple
409 paragraphs, as separated by empty lines.
409 paragraphs, as separated by empty lines.
410
410
411 Returns
411 Returns
412 -------
412 -------
413
413
414 list of complete paragraphs, wrapped to fill `ncols` columns.
414 list of complete paragraphs, wrapped to fill `ncols` columns.
415 """
415 """
416 paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE)
416 paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE)
417 text = dedent(text).strip()
417 text = dedent(text).strip()
418 paragraphs = paragraph_re.split(text)[::2] # every other entry is space
418 paragraphs = paragraph_re.split(text)[::2] # every other entry is space
419 out_ps = []
419 out_ps = []
420 indent_re = re.compile(r'\n\s+', re.MULTILINE)
420 indent_re = re.compile(r'\n\s+', re.MULTILINE)
421 for p in paragraphs:
421 for p in paragraphs:
422 # presume indentation that survives dedent is meaningful formatting,
422 # presume indentation that survives dedent is meaningful formatting,
423 # so don't fill unless text is flush.
423 # so don't fill unless text is flush.
424 if indent_re.search(p) is None:
424 if indent_re.search(p) is None:
425 # wrap paragraph
425 # wrap paragraph
426 p = textwrap.fill(p, ncols)
426 p = textwrap.fill(p, ncols)
427 out_ps.append(p)
427 out_ps.append(p)
428 return out_ps
428 return out_ps
429
429
430
430
431 def long_substr(data):
431 def long_substr(data):
432 """Return the longest common substring in a list of strings.
432 """Return the longest common substring in a list of strings.
433
433
434 Credit: http://stackoverflow.com/questions/2892931/longest-common-substring-from-more-than-two-strings-python
434 Credit: http://stackoverflow.com/questions/2892931/longest-common-substring-from-more-than-two-strings-python
435 """
435 """
436 substr = ''
436 substr = ''
437 if len(data) > 1 and len(data[0]) > 0:
437 if len(data) > 1 and len(data[0]) > 0:
438 for i in range(len(data[0])):
438 for i in range(len(data[0])):
439 for j in range(len(data[0])-i+1):
439 for j in range(len(data[0])-i+1):
440 if j > len(substr) and all(data[0][i:i+j] in x for x in data):
440 if j > len(substr) and all(data[0][i:i+j] in x for x in data):
441 substr = data[0][i:i+j]
441 substr = data[0][i:i+j]
442 elif len(data) == 1:
442 elif len(data) == 1:
443 substr = data[0]
443 substr = data[0]
444 return substr
444 return substr
445
445
446
446
447 def strip_email_quotes(text):
447 def strip_email_quotes(text):
448 """Strip leading email quotation characters ('>').
448 """Strip leading email quotation characters ('>').
449
449
450 Removes any combination of leading '>' interspersed with whitespace that
450 Removes any combination of leading '>' interspersed with whitespace that
451 appears *identically* in all lines of the input text.
451 appears *identically* in all lines of the input text.
452
452
453 Parameters
453 Parameters
454 ----------
454 ----------
455 text : str
455 text : str
456
456
457 Examples
457 Examples
458 --------
458 --------
459
459
460 Simple uses::
460 Simple uses::
461
461
462 In [2]: strip_email_quotes('> > text')
462 In [2]: strip_email_quotes('> > text')
463 Out[2]: 'text'
463 Out[2]: 'text'
464
464
465 In [3]: strip_email_quotes('> > text\\n> > more')
465 In [3]: strip_email_quotes('> > text\\n> > more')
466 Out[3]: 'text\\nmore'
466 Out[3]: 'text\\nmore'
467
467
468 Note how only the common prefix that appears in all lines is stripped::
468 Note how only the common prefix that appears in all lines is stripped::
469
469
470 In [4]: strip_email_quotes('> > text\\n> > more\\n> more...')
470 In [4]: strip_email_quotes('> > text\\n> > more\\n> more...')
471 Out[4]: '> text\\n> more\\nmore...'
471 Out[4]: '> text\\n> more\\nmore...'
472
472
473 So if any line has no quote marks ('>') , then none are stripped from any
473 So if any line has no quote marks ('>') , then none are stripped from any
474 of them ::
474 of them ::
475
475
476 In [5]: strip_email_quotes('> > text\\n> > more\\nlast different')
476 In [5]: strip_email_quotes('> > text\\n> > more\\nlast different')
477 Out[5]: '> > text\\n> > more\\nlast different'
477 Out[5]: '> > text\\n> > more\\nlast different'
478 """
478 """
479 lines = text.splitlines()
479 lines = text.splitlines()
480 matches = set()
480 matches = set()
481 for line in lines:
481 for line in lines:
482 prefix = re.match(r'^(\s*>[ >]*)', line)
482 prefix = re.match(r'^(\s*>[ >]*)', line)
483 if prefix:
483 if prefix:
484 matches.add(prefix.group(1))
484 matches.add(prefix.group(1))
485 else:
485 else:
486 break
486 break
487 else:
487 else:
488 prefix = long_substr(list(matches))
488 prefix = long_substr(list(matches))
489 if prefix:
489 if prefix:
490 strip = len(prefix)
490 strip = len(prefix)
491 text = '\n'.join([ ln[strip:] for ln in lines])
491 text = '\n'.join([ ln[strip:] for ln in lines])
492 return text
492 return text
493
493
494 def strip_ansi(source):
495 """
496 Remove ansi escape codes from text.
497
498 Parameters
499 ----------
500 source : str
501 Source to remove the ansi from
502 """
503 return re.sub(r'\033\[(\d|;)+?m', '', source)
504
494
505
495 class EvalFormatter(Formatter):
506 class EvalFormatter(Formatter):
496 """A String Formatter that allows evaluation of simple expressions.
507 """A String Formatter that allows evaluation of simple expressions.
497
508
498 Note that this version interprets a : as specifying a format string (as per
509 Note that this version interprets a : as specifying a format string (as per
499 standard string formatting), so if slicing is required, you must explicitly
510 standard string formatting), so if slicing is required, you must explicitly
500 create a slice.
511 create a slice.
501
512
502 This is to be used in templating cases, such as the parallel batch
513 This is to be used in templating cases, such as the parallel batch
503 script templates, where simple arithmetic on arguments is useful.
514 script templates, where simple arithmetic on arguments is useful.
504
515
505 Examples
516 Examples
506 --------
517 --------
507 ::
518 ::
508
519
509 In [1]: f = EvalFormatter()
520 In [1]: f = EvalFormatter()
510 In [2]: f.format('{n//4}', n=8)
521 In [2]: f.format('{n//4}', n=8)
511 Out[2]: '2'
522 Out[2]: '2'
512
523
513 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
524 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
514 Out[3]: 'll'
525 Out[3]: 'll'
515 """
526 """
516 def get_field(self, name, args, kwargs):
527 def get_field(self, name, args, kwargs):
517 v = eval(name, kwargs)
528 v = eval(name, kwargs)
518 return v, name
529 return v, name
519
530
520 #XXX: As of Python 3.4, the format string parsing no longer splits on a colon
531 #XXX: As of Python 3.4, the format string parsing no longer splits on a colon
521 # inside [], so EvalFormatter can handle slicing. Once we only support 3.4 and
532 # inside [], so EvalFormatter can handle slicing. Once we only support 3.4 and
522 # above, it should be possible to remove FullEvalFormatter.
533 # above, it should be possible to remove FullEvalFormatter.
523
534
524 @skip_doctest_py3
535 @skip_doctest_py3
525 class FullEvalFormatter(Formatter):
536 class FullEvalFormatter(Formatter):
526 """A String Formatter that allows evaluation of simple expressions.
537 """A String Formatter that allows evaluation of simple expressions.
527
538
528 Any time a format key is not found in the kwargs,
539 Any time a format key is not found in the kwargs,
529 it will be tried as an expression in the kwargs namespace.
540 it will be tried as an expression in the kwargs namespace.
530
541
531 Note that this version allows slicing using [1:2], so you cannot specify
542 Note that this version allows slicing using [1:2], so you cannot specify
532 a format string. Use :class:`EvalFormatter` to permit format strings.
543 a format string. Use :class:`EvalFormatter` to permit format strings.
533
544
534 Examples
545 Examples
535 --------
546 --------
536 ::
547 ::
537
548
538 In [1]: f = FullEvalFormatter()
549 In [1]: f = FullEvalFormatter()
539 In [2]: f.format('{n//4}', n=8)
550 In [2]: f.format('{n//4}', n=8)
540 Out[2]: u'2'
551 Out[2]: u'2'
541
552
542 In [3]: f.format('{list(range(5))[2:4]}')
553 In [3]: f.format('{list(range(5))[2:4]}')
543 Out[3]: u'[2, 3]'
554 Out[3]: u'[2, 3]'
544
555
545 In [4]: f.format('{3*2}')
556 In [4]: f.format('{3*2}')
546 Out[4]: u'6'
557 Out[4]: u'6'
547 """
558 """
548 # copied from Formatter._vformat with minor changes to allow eval
559 # copied from Formatter._vformat with minor changes to allow eval
549 # and replace the format_spec code with slicing
560 # and replace the format_spec code with slicing
550 def _vformat(self, format_string, args, kwargs, used_args, recursion_depth):
561 def _vformat(self, format_string, args, kwargs, used_args, recursion_depth):
551 if recursion_depth < 0:
562 if recursion_depth < 0:
552 raise ValueError('Max string recursion exceeded')
563 raise ValueError('Max string recursion exceeded')
553 result = []
564 result = []
554 for literal_text, field_name, format_spec, conversion in \
565 for literal_text, field_name, format_spec, conversion in \
555 self.parse(format_string):
566 self.parse(format_string):
556
567
557 # output the literal text
568 # output the literal text
558 if literal_text:
569 if literal_text:
559 result.append(literal_text)
570 result.append(literal_text)
560
571
561 # if there's a field, output it
572 # if there's a field, output it
562 if field_name is not None:
573 if field_name is not None:
563 # this is some markup, find the object and do
574 # this is some markup, find the object and do
564 # the formatting
575 # the formatting
565
576
566 if format_spec:
577 if format_spec:
567 # override format spec, to allow slicing:
578 # override format spec, to allow slicing:
568 field_name = ':'.join([field_name, format_spec])
579 field_name = ':'.join([field_name, format_spec])
569
580
570 # eval the contents of the field for the object
581 # eval the contents of the field for the object
571 # to be formatted
582 # to be formatted
572 obj = eval(field_name, kwargs)
583 obj = eval(field_name, kwargs)
573
584
574 # do any conversion on the resulting object
585 # do any conversion on the resulting object
575 obj = self.convert_field(obj, conversion)
586 obj = self.convert_field(obj, conversion)
576
587
577 # format the object and append to the result
588 # format the object and append to the result
578 result.append(self.format_field(obj, ''))
589 result.append(self.format_field(obj, ''))
579
590
580 return u''.join(py3compat.cast_unicode(s) for s in result)
591 return u''.join(py3compat.cast_unicode(s) for s in result)
581
592
582
593
583 @skip_doctest_py3
594 @skip_doctest_py3
584 class DollarFormatter(FullEvalFormatter):
595 class DollarFormatter(FullEvalFormatter):
585 """Formatter allowing Itpl style $foo replacement, for names and attribute
596 """Formatter allowing Itpl style $foo replacement, for names and attribute
586 access only. Standard {foo} replacement also works, and allows full
597 access only. Standard {foo} replacement also works, and allows full
587 evaluation of its arguments.
598 evaluation of its arguments.
588
599
589 Examples
600 Examples
590 --------
601 --------
591 ::
602 ::
592
603
593 In [1]: f = DollarFormatter()
604 In [1]: f = DollarFormatter()
594 In [2]: f.format('{n//4}', n=8)
605 In [2]: f.format('{n//4}', n=8)
595 Out[2]: u'2'
606 Out[2]: u'2'
596
607
597 In [3]: f.format('23 * 76 is $result', result=23*76)
608 In [3]: f.format('23 * 76 is $result', result=23*76)
598 Out[3]: u'23 * 76 is 1748'
609 Out[3]: u'23 * 76 is 1748'
599
610
600 In [4]: f.format('$a or {b}', a=1, b=2)
611 In [4]: f.format('$a or {b}', a=1, b=2)
601 Out[4]: u'1 or 2'
612 Out[4]: u'1 or 2'
602 """
613 """
603 _dollar_pattern = re.compile("(.*?)\$(\$?[\w\.]+)")
614 _dollar_pattern = re.compile("(.*?)\$(\$?[\w\.]+)")
604 def parse(self, fmt_string):
615 def parse(self, fmt_string):
605 for literal_txt, field_name, format_spec, conversion \
616 for literal_txt, field_name, format_spec, conversion \
606 in Formatter.parse(self, fmt_string):
617 in Formatter.parse(self, fmt_string):
607
618
608 # Find $foo patterns in the literal text.
619 # Find $foo patterns in the literal text.
609 continue_from = 0
620 continue_from = 0
610 txt = ""
621 txt = ""
611 for m in self._dollar_pattern.finditer(literal_txt):
622 for m in self._dollar_pattern.finditer(literal_txt):
612 new_txt, new_field = m.group(1,2)
623 new_txt, new_field = m.group(1,2)
613 # $$foo --> $foo
624 # $$foo --> $foo
614 if new_field.startswith("$"):
625 if new_field.startswith("$"):
615 txt += new_txt + new_field
626 txt += new_txt + new_field
616 else:
627 else:
617 yield (txt + new_txt, new_field, "", None)
628 yield (txt + new_txt, new_field, "", None)
618 txt = ""
629 txt = ""
619 continue_from = m.end()
630 continue_from = m.end()
620
631
621 # Re-yield the {foo} style pattern
632 # Re-yield the {foo} style pattern
622 yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
633 yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
623
634
624 #-----------------------------------------------------------------------------
635 #-----------------------------------------------------------------------------
625 # Utils to columnize a list of string
636 # Utils to columnize a list of string
626 #-----------------------------------------------------------------------------
637 #-----------------------------------------------------------------------------
627
638
628 def _chunks(l, n):
639 def _chunks(l, n):
629 """Yield successive n-sized chunks from l."""
640 """Yield successive n-sized chunks from l."""
630 for i in py3compat.xrange(0, len(l), n):
641 for i in py3compat.xrange(0, len(l), n):
631 yield l[i:i+n]
642 yield l[i:i+n]
632
643
633
644
634 def _find_optimal(rlist , separator_size=2 , displaywidth=80):
645 def _find_optimal(rlist , separator_size=2 , displaywidth=80):
635 """Calculate optimal info to columnize a list of string"""
646 """Calculate optimal info to columnize a list of string"""
636 for nrow in range(1, len(rlist)+1) :
647 for nrow in range(1, len(rlist)+1) :
637 chk = list(map(max,_chunks(rlist, nrow)))
648 chk = list(map(max,_chunks(rlist, nrow)))
638 sumlength = sum(chk)
649 sumlength = sum(chk)
639 ncols = len(chk)
650 ncols = len(chk)
640 if sumlength+separator_size*(ncols-1) <= displaywidth :
651 if sumlength+separator_size*(ncols-1) <= displaywidth :
641 break;
652 break;
642 return {'columns_numbers' : ncols,
653 return {'columns_numbers' : ncols,
643 'optimal_separator_width':(displaywidth - sumlength)/(ncols-1) if (ncols -1) else 0,
654 'optimal_separator_width':(displaywidth - sumlength)/(ncols-1) if (ncols -1) else 0,
644 'rows_numbers' : nrow,
655 'rows_numbers' : nrow,
645 'columns_width' : chk
656 'columns_width' : chk
646 }
657 }
647
658
648
659
649 def _get_or_default(mylist, i, default=None):
660 def _get_or_default(mylist, i, default=None):
650 """return list item number, or default if don't exist"""
661 """return list item number, or default if don't exist"""
651 if i >= len(mylist):
662 if i >= len(mylist):
652 return default
663 return default
653 else :
664 else :
654 return mylist[i]
665 return mylist[i]
655
666
656
667
657 @skip_doctest
668 @skip_doctest
658 def compute_item_matrix(items, empty=None, *args, **kwargs) :
669 def compute_item_matrix(items, empty=None, *args, **kwargs) :
659 """Returns a nested list, and info to columnize items
670 """Returns a nested list, and info to columnize items
660
671
661 Parameters
672 Parameters
662 ----------
673 ----------
663
674
664 items
675 items
665 list of strings to columize
676 list of strings to columize
666 empty : (default None)
677 empty : (default None)
667 default value to fill list if needed
678 default value to fill list if needed
668 separator_size : int (default=2)
679 separator_size : int (default=2)
669 How much caracters will be used as a separation between each columns.
680 How much caracters will be used as a separation between each columns.
670 displaywidth : int (default=80)
681 displaywidth : int (default=80)
671 The width of the area onto wich the columns should enter
682 The width of the area onto wich the columns should enter
672
683
673 Returns
684 Returns
674 -------
685 -------
675
686
676 strings_matrix
687 strings_matrix
677
688
678 nested list of string, the outer most list contains as many list as
689 nested list of string, the outer most list contains as many list as
679 rows, the innermost lists have each as many element as colums. If the
690 rows, the innermost lists have each as many element as colums. If the
680 total number of elements in `items` does not equal the product of
691 total number of elements in `items` does not equal the product of
681 rows*columns, the last element of some lists are filled with `None`.
692 rows*columns, the last element of some lists are filled with `None`.
682
693
683 dict_info
694 dict_info
684 some info to make columnize easier:
695 some info to make columnize easier:
685
696
686 columns_numbers
697 columns_numbers
687 number of columns
698 number of columns
688 rows_numbers
699 rows_numbers
689 number of rows
700 number of rows
690 columns_width
701 columns_width
691 list of with of each columns
702 list of with of each columns
692 optimal_separator_width
703 optimal_separator_width
693 best separator width between columns
704 best separator width between columns
694
705
695 Examples
706 Examples
696 --------
707 --------
697 ::
708 ::
698
709
699 In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l']
710 In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l']
700 ...: compute_item_matrix(l,displaywidth=12)
711 ...: compute_item_matrix(l,displaywidth=12)
701 Out[1]:
712 Out[1]:
702 ([['aaa', 'f', 'k'],
713 ([['aaa', 'f', 'k'],
703 ['b', 'g', 'l'],
714 ['b', 'g', 'l'],
704 ['cc', 'h', None],
715 ['cc', 'h', None],
705 ['d', 'i', None],
716 ['d', 'i', None],
706 ['eeeee', 'j', None]],
717 ['eeeee', 'j', None]],
707 {'columns_numbers': 3,
718 {'columns_numbers': 3,
708 'columns_width': [5, 1, 1],
719 'columns_width': [5, 1, 1],
709 'optimal_separator_width': 2,
720 'optimal_separator_width': 2,
710 'rows_numbers': 5})
721 'rows_numbers': 5})
711 """
722 """
712 info = _find_optimal(list(map(len, items)), *args, **kwargs)
723 info = _find_optimal(list(map(len, items)), *args, **kwargs)
713 nrow, ncol = info['rows_numbers'], info['columns_numbers']
724 nrow, ncol = info['rows_numbers'], info['columns_numbers']
714 return ([[ _get_or_default(items, c*nrow+i, default=empty) for c in range(ncol) ] for i in range(nrow) ], info)
725 return ([[ _get_or_default(items, c*nrow+i, default=empty) for c in range(ncol) ] for i in range(nrow) ], info)
715
726
716
727
717 def columnize(items, separator=' ', displaywidth=80):
728 def columnize(items, separator=' ', displaywidth=80):
718 """ Transform a list of strings into a single string with columns.
729 """ Transform a list of strings into a single string with columns.
719
730
720 Parameters
731 Parameters
721 ----------
732 ----------
722 items : sequence of strings
733 items : sequence of strings
723 The strings to process.
734 The strings to process.
724
735
725 separator : str, optional [default is two spaces]
736 separator : str, optional [default is two spaces]
726 The string that separates columns.
737 The string that separates columns.
727
738
728 displaywidth : int, optional [default is 80]
739 displaywidth : int, optional [default is 80]
729 Width of the display in number of characters.
740 Width of the display in number of characters.
730
741
731 Returns
742 Returns
732 -------
743 -------
733 The formatted string.
744 The formatted string.
734 """
745 """
735 if not items :
746 if not items :
736 return '\n'
747 return '\n'
737 matrix, info = compute_item_matrix(items, separator_size=len(separator), displaywidth=displaywidth)
748 matrix, info = compute_item_matrix(items, separator_size=len(separator), displaywidth=displaywidth)
738 fmatrix = [filter(None, x) for x in matrix]
749 fmatrix = [filter(None, x) for x in matrix]
739 sjoin = lambda x : separator.join([ y.ljust(w, ' ') for y, w in zip(x, info['columns_width'])])
750 sjoin = lambda x : separator.join([ y.ljust(w, ' ') for y, w in zip(x, info['columns_width'])])
740 return '\n'.join(map(sjoin, fmatrix))+'\n'
751 return '\n'.join(map(sjoin, fmatrix))+'\n'
741
752
742
753
743 def get_text_list(list_, last_sep=' and ', sep=", ", wrap_item_with=""):
754 def get_text_list(list_, last_sep=' and ', sep=", ", wrap_item_with=""):
744 """
755 """
745 Return a string with a natural enumeration of items
756 Return a string with a natural enumeration of items
746
757
747 >>> get_text_list(['a', 'b', 'c', 'd'])
758 >>> get_text_list(['a', 'b', 'c', 'd'])
748 'a, b, c and d'
759 'a, b, c and d'
749 >>> get_text_list(['a', 'b', 'c'], ' or ')
760 >>> get_text_list(['a', 'b', 'c'], ' or ')
750 'a, b or c'
761 'a, b or c'
751 >>> get_text_list(['a', 'b', 'c'], ', ')
762 >>> get_text_list(['a', 'b', 'c'], ', ')
752 'a, b, c'
763 'a, b, c'
753 >>> get_text_list(['a', 'b'], ' or ')
764 >>> get_text_list(['a', 'b'], ' or ')
754 'a or b'
765 'a or b'
755 >>> get_text_list(['a'])
766 >>> get_text_list(['a'])
756 'a'
767 'a'
757 >>> get_text_list([])
768 >>> get_text_list([])
758 ''
769 ''
759 >>> get_text_list(['a', 'b'], wrap_item_with="`")
770 >>> get_text_list(['a', 'b'], wrap_item_with="`")
760 '`a` and `b`'
771 '`a` and `b`'
761 >>> get_text_list(['a', 'b', 'c', 'd'], " = ", sep=" + ")
772 >>> get_text_list(['a', 'b', 'c', 'd'], " = ", sep=" + ")
762 'a + b + c = d'
773 'a + b + c = d'
763 """
774 """
764 if len(list_) == 0:
775 if len(list_) == 0:
765 return ''
776 return ''
766 if wrap_item_with:
777 if wrap_item_with:
767 list_ = ['%s%s%s' % (wrap_item_with, item, wrap_item_with) for
778 list_ = ['%s%s%s' % (wrap_item_with, item, wrap_item_with) for
768 item in list_]
779 item in list_]
769 if len(list_) == 1:
780 if len(list_) == 1:
770 return list_[0]
781 return list_[0]
771 return '%s%s%s' % (
782 return '%s%s%s' % (
772 sep.join(i for i in list_[:-1]),
783 sep.join(i for i in list_[:-1]),
773 last_sep, list_[-1]) No newline at end of file
784 last_sep, list_[-1])
General Comments 0
You need to be logged in to leave comments. Login now