##// END OF EJS Templates
Fix typos
Andrew Kreimer -
Show More
@@ -1,828 +1,828
1 """
1 """
2 Utilities for working with strings and text.
2 Utilities for working with strings and text.
3
3
4 Inheritance diagram:
4 Inheritance diagram:
5
5
6 .. inheritance-diagram:: IPython.utils.text
6 .. inheritance-diagram:: IPython.utils.text
7 :parts: 3
7 :parts: 3
8 """
8 """
9
9
10 import os
10 import os
11 import re
11 import re
12 import string
12 import string
13 import sys
13 import sys
14 import textwrap
14 import textwrap
15 import warnings
15 import warnings
16 from string import Formatter
16 from string import Formatter
17 from pathlib import Path
17 from pathlib import Path
18
18
19 from typing import List, Dict, Tuple, Optional, cast, Sequence, Mapping, Any
19 from typing import List, Dict, Tuple, Optional, cast, Sequence, Mapping, Any
20
20
21 if sys.version_info < (3, 12):
21 if sys.version_info < (3, 12):
22 from typing_extensions import Self
22 from typing_extensions import Self
23 else:
23 else:
24 from typing import Self
24 from typing import Self
25
25
26
26
27 class LSString(str):
27 class LSString(str):
28 """String derivative with a special access attributes.
28 """String derivative with a special access attributes.
29
29
30 These are normal strings, but with the special attributes:
30 These are normal strings, but with the special attributes:
31
31
32 .l (or .list) : value as list (split on newlines).
32 .l (or .list) : value as list (split on newlines).
33 .n (or .nlstr): original value (the string itself).
33 .n (or .nlstr): original value (the string itself).
34 .s (or .spstr): value as whitespace-separated string.
34 .s (or .spstr): value as whitespace-separated string.
35 .p (or .paths): list of path objects (requires path.py package)
35 .p (or .paths): list of path objects (requires path.py package)
36
36
37 Any values which require transformations are computed only once and
37 Any values which require transformations are computed only once and
38 cached.
38 cached.
39
39
40 Such strings are very useful to efficiently interact with the shell, which
40 Such strings are very useful to efficiently interact with the shell, which
41 typically only understands whitespace-separated options for commands."""
41 typically only understands whitespace-separated options for commands."""
42
42
43 __list: List[str]
43 __list: List[str]
44 __spstr: str
44 __spstr: str
45 __paths: List[Path]
45 __paths: List[Path]
46
46
47 def get_list(self) -> List[str]:
47 def get_list(self) -> List[str]:
48 try:
48 try:
49 return self.__list
49 return self.__list
50 except AttributeError:
50 except AttributeError:
51 self.__list = self.split('\n')
51 self.__list = self.split('\n')
52 return self.__list
52 return self.__list
53
53
54 l = list = property(get_list)
54 l = list = property(get_list)
55
55
56 def get_spstr(self) -> str:
56 def get_spstr(self) -> str:
57 try:
57 try:
58 return self.__spstr
58 return self.__spstr
59 except AttributeError:
59 except AttributeError:
60 self.__spstr = self.replace('\n',' ')
60 self.__spstr = self.replace('\n',' ')
61 return self.__spstr
61 return self.__spstr
62
62
63 s = spstr = property(get_spstr)
63 s = spstr = property(get_spstr)
64
64
65 def get_nlstr(self) -> Self:
65 def get_nlstr(self) -> Self:
66 return self
66 return self
67
67
68 n = nlstr = property(get_nlstr)
68 n = nlstr = property(get_nlstr)
69
69
70 def get_paths(self) -> List[Path]:
70 def get_paths(self) -> List[Path]:
71 try:
71 try:
72 return self.__paths
72 return self.__paths
73 except AttributeError:
73 except AttributeError:
74 self.__paths = [Path(p) for p in self.split('\n') if os.path.exists(p)]
74 self.__paths = [Path(p) for p in self.split('\n') if os.path.exists(p)]
75 return self.__paths
75 return self.__paths
76
76
77 p = paths = property(get_paths)
77 p = paths = property(get_paths)
78
78
79 # FIXME: We need to reimplement type specific displayhook and then add this
79 # FIXME: We need to reimplement type specific displayhook and then add this
80 # back as a custom printer. This should also be moved outside utils into the
80 # back as a custom printer. This should also be moved outside utils into the
81 # core.
81 # core.
82
82
83 # def print_lsstring(arg):
83 # def print_lsstring(arg):
84 # """ Prettier (non-repr-like) and more informative printer for LSString """
84 # """ Prettier (non-repr-like) and more informative printer for LSString """
85 # print("LSString (.p, .n, .l, .s available). Value:")
85 # print("LSString (.p, .n, .l, .s available). Value:")
86 # print(arg)
86 # print(arg)
87 #
87 #
88 #
88 #
89 # print_lsstring = result_display.register(LSString)(print_lsstring)
89 # print_lsstring = result_display.register(LSString)(print_lsstring)
90
90
91
91
92 class SList(list):
92 class SList(list):
93 """List derivative with a special access attributes.
93 """List derivative with a special access attributes.
94
94
95 These are normal lists, but with the special attributes:
95 These are normal lists, but with the special attributes:
96
96
97 * .l (or .list) : value as list (the list itself).
97 * .l (or .list) : value as list (the list itself).
98 * .n (or .nlstr): value as a string, joined on newlines.
98 * .n (or .nlstr): value as a string, joined on newlines.
99 * .s (or .spstr): value as a string, joined on spaces.
99 * .s (or .spstr): value as a string, joined on spaces.
100 * .p (or .paths): list of path objects (requires path.py package)
100 * .p (or .paths): list of path objects (requires path.py package)
101
101
102 Any values which require transformations are computed only once and
102 Any values which require transformations are computed only once and
103 cached."""
103 cached."""
104
104
105 __spstr: str
105 __spstr: str
106 __nlstr: str
106 __nlstr: str
107 __paths: List[Path]
107 __paths: List[Path]
108
108
109 def get_list(self) -> Self:
109 def get_list(self) -> Self:
110 return self
110 return self
111
111
112 l = list = property(get_list)
112 l = list = property(get_list)
113
113
114 def get_spstr(self) -> str:
114 def get_spstr(self) -> str:
115 try:
115 try:
116 return self.__spstr
116 return self.__spstr
117 except AttributeError:
117 except AttributeError:
118 self.__spstr = ' '.join(self)
118 self.__spstr = ' '.join(self)
119 return self.__spstr
119 return self.__spstr
120
120
121 s = spstr = property(get_spstr)
121 s = spstr = property(get_spstr)
122
122
123 def get_nlstr(self) -> str:
123 def get_nlstr(self) -> str:
124 try:
124 try:
125 return self.__nlstr
125 return self.__nlstr
126 except AttributeError:
126 except AttributeError:
127 self.__nlstr = '\n'.join(self)
127 self.__nlstr = '\n'.join(self)
128 return self.__nlstr
128 return self.__nlstr
129
129
130 n = nlstr = property(get_nlstr)
130 n = nlstr = property(get_nlstr)
131
131
132 def get_paths(self) -> List[Path]:
132 def get_paths(self) -> List[Path]:
133 try:
133 try:
134 return self.__paths
134 return self.__paths
135 except AttributeError:
135 except AttributeError:
136 self.__paths = [Path(p) for p in self if os.path.exists(p)]
136 self.__paths = [Path(p) for p in self if os.path.exists(p)]
137 return self.__paths
137 return self.__paths
138
138
139 p = paths = property(get_paths)
139 p = paths = property(get_paths)
140
140
141 def grep(self, pattern, prune = False, field = None):
141 def grep(self, pattern, prune = False, field = None):
142 """ Return all strings matching 'pattern' (a regex or callable)
142 """ Return all strings matching 'pattern' (a regex or callable)
143
143
144 This is case-insensitive. If prune is true, return all items
144 This is case-insensitive. If prune is true, return all items
145 NOT matching the pattern.
145 NOT matching the pattern.
146
146
147 If field is specified, the match must occur in the specified
147 If field is specified, the match must occur in the specified
148 whitespace-separated field.
148 whitespace-separated field.
149
149
150 Examples::
150 Examples::
151
151
152 a.grep( lambda x: x.startswith('C') )
152 a.grep( lambda x: x.startswith('C') )
153 a.grep('Cha.*log', prune=1)
153 a.grep('Cha.*log', prune=1)
154 a.grep('chm', field=-1)
154 a.grep('chm', field=-1)
155 """
155 """
156
156
157 def match_target(s):
157 def match_target(s):
158 if field is None:
158 if field is None:
159 return s
159 return s
160 parts = s.split()
160 parts = s.split()
161 try:
161 try:
162 tgt = parts[field]
162 tgt = parts[field]
163 return tgt
163 return tgt
164 except IndexError:
164 except IndexError:
165 return ""
165 return ""
166
166
167 if isinstance(pattern, str):
167 if isinstance(pattern, str):
168 pred = lambda x : re.search(pattern, x, re.IGNORECASE)
168 pred = lambda x : re.search(pattern, x, re.IGNORECASE)
169 else:
169 else:
170 pred = pattern
170 pred = pattern
171 if not prune:
171 if not prune:
172 return SList([el for el in self if pred(match_target(el))])
172 return SList([el for el in self if pred(match_target(el))])
173 else:
173 else:
174 return SList([el for el in self if not pred(match_target(el))])
174 return SList([el for el in self if not pred(match_target(el))])
175
175
176 def fields(self, *fields):
176 def fields(self, *fields):
177 """ Collect whitespace-separated fields from string list
177 """ Collect whitespace-separated fields from string list
178
178
179 Allows quick awk-like usage of string lists.
179 Allows quick awk-like usage of string lists.
180
180
181 Example data (in var a, created by 'a = !ls -l')::
181 Example data (in var a, created by 'a = !ls -l')::
182
182
183 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
183 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
184 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
184 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
185
185
186 * ``a.fields(0)`` is ``['-rwxrwxrwx', 'drwxrwxrwx+']``
186 * ``a.fields(0)`` is ``['-rwxrwxrwx', 'drwxrwxrwx+']``
187 * ``a.fields(1,0)`` is ``['1 -rwxrwxrwx', '6 drwxrwxrwx+']``
187 * ``a.fields(1,0)`` is ``['1 -rwxrwxrwx', '6 drwxrwxrwx+']``
188 (note the joining by space).
188 (note the joining by space).
189 * ``a.fields(-1)`` is ``['ChangeLog', 'IPython']``
189 * ``a.fields(-1)`` is ``['ChangeLog', 'IPython']``
190
190
191 IndexErrors are ignored.
191 IndexErrors are ignored.
192
192
193 Without args, fields() just split()'s the strings.
193 Without args, fields() just split()'s the strings.
194 """
194 """
195 if len(fields) == 0:
195 if len(fields) == 0:
196 return [el.split() for el in self]
196 return [el.split() for el in self]
197
197
198 res = SList()
198 res = SList()
199 for el in [f.split() for f in self]:
199 for el in [f.split() for f in self]:
200 lineparts = []
200 lineparts = []
201
201
202 for fd in fields:
202 for fd in fields:
203 try:
203 try:
204 lineparts.append(el[fd])
204 lineparts.append(el[fd])
205 except IndexError:
205 except IndexError:
206 pass
206 pass
207 if lineparts:
207 if lineparts:
208 res.append(" ".join(lineparts))
208 res.append(" ".join(lineparts))
209
209
210 return res
210 return res
211
211
212 def sort(self,field= None, nums = False):
212 def sort(self,field= None, nums = False):
213 """ sort by specified fields (see fields())
213 """ sort by specified fields (see fields())
214
214
215 Example::
215 Example::
216
216
217 a.sort(1, nums = True)
217 a.sort(1, nums = True)
218
218
219 Sorts a by second field, in numerical order (so that 21 > 3)
219 Sorts a by second field, in numerical order (so that 21 > 3)
220
220
221 """
221 """
222
222
223 #decorate, sort, undecorate
223 #decorate, sort, undecorate
224 if field is not None:
224 if field is not None:
225 dsu = [[SList([line]).fields(field), line] for line in self]
225 dsu = [[SList([line]).fields(field), line] for line in self]
226 else:
226 else:
227 dsu = [[line, line] for line in self]
227 dsu = [[line, line] for line in self]
228 if nums:
228 if nums:
229 for i in range(len(dsu)):
229 for i in range(len(dsu)):
230 numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
230 numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
231 try:
231 try:
232 n = int(numstr)
232 n = int(numstr)
233 except ValueError:
233 except ValueError:
234 n = 0
234 n = 0
235 dsu[i][0] = n
235 dsu[i][0] = n
236
236
237
237
238 dsu.sort()
238 dsu.sort()
239 return SList([t[1] for t in dsu])
239 return SList([t[1] for t in dsu])
240
240
241
241
242 # FIXME: We need to reimplement type specific displayhook and then add this
242 # FIXME: We need to reimplement type specific displayhook and then add this
243 # back as a custom printer. This should also be moved outside utils into the
243 # back as a custom printer. This should also be moved outside utils into the
244 # core.
244 # core.
245
245
246 # def print_slist(arg):
246 # def print_slist(arg):
247 # """ Prettier (non-repr-like) and more informative printer for SList """
247 # """ Prettier (non-repr-like) and more informative printer for SList """
248 # print("SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):")
248 # print("SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):")
249 # if hasattr(arg, 'hideonce') and arg.hideonce:
249 # if hasattr(arg, 'hideonce') and arg.hideonce:
250 # arg.hideonce = False
250 # arg.hideonce = False
251 # return
251 # return
252 #
252 #
253 # nlprint(arg) # This was a nested list printer, now removed.
253 # nlprint(arg) # This was a nested list printer, now removed.
254 #
254 #
255 # print_slist = result_display.register(SList)(print_slist)
255 # print_slist = result_display.register(SList)(print_slist)
256
256
257
257
258 def indent(instr,nspaces=4, ntabs=0, flatten=False):
258 def indent(instr,nspaces=4, ntabs=0, flatten=False):
259 """Indent a string a given number of spaces or tabstops.
259 """Indent a string a given number of spaces or tabstops.
260
260
261 indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces.
261 indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces.
262
262
263 Parameters
263 Parameters
264 ----------
264 ----------
265 instr : basestring
265 instr : basestring
266 The string to be indented.
266 The string to be indented.
267 nspaces : int (default: 4)
267 nspaces : int (default: 4)
268 The number of spaces to be indented.
268 The number of spaces to be indented.
269 ntabs : int (default: 0)
269 ntabs : int (default: 0)
270 The number of tabs to be indented.
270 The number of tabs to be indented.
271 flatten : bool (default: False)
271 flatten : bool (default: False)
272 Whether to scrub existing indentation. If True, all lines will be
272 Whether to scrub existing indentation. If True, all lines will be
273 aligned to the same indentation. If False, existing indentation will
273 aligned to the same indentation. If False, existing indentation will
274 be strictly increased.
274 be strictly increased.
275
275
276 Returns
276 Returns
277 -------
277 -------
278 str|unicode : string indented by ntabs and nspaces.
278 str|unicode : string indented by ntabs and nspaces.
279
279
280 """
280 """
281 if instr is None:
281 if instr is None:
282 return
282 return
283 ind = '\t'*ntabs+' '*nspaces
283 ind = '\t'*ntabs+' '*nspaces
284 if flatten:
284 if flatten:
285 pat = re.compile(r'^\s*', re.MULTILINE)
285 pat = re.compile(r'^\s*', re.MULTILINE)
286 else:
286 else:
287 pat = re.compile(r'^', re.MULTILINE)
287 pat = re.compile(r'^', re.MULTILINE)
288 outstr = re.sub(pat, ind, instr)
288 outstr = re.sub(pat, ind, instr)
289 if outstr.endswith(os.linesep+ind):
289 if outstr.endswith(os.linesep+ind):
290 return outstr[:-len(ind)]
290 return outstr[:-len(ind)]
291 else:
291 else:
292 return outstr
292 return outstr
293
293
294
294
295 def list_strings(arg):
295 def list_strings(arg):
296 """Always return a list of strings, given a string or list of strings
296 """Always return a list of strings, given a string or list of strings
297 as input.
297 as input.
298
298
299 Examples
299 Examples
300 --------
300 --------
301 ::
301 ::
302
302
303 In [7]: list_strings('A single string')
303 In [7]: list_strings('A single string')
304 Out[7]: ['A single string']
304 Out[7]: ['A single string']
305
305
306 In [8]: list_strings(['A single string in a list'])
306 In [8]: list_strings(['A single string in a list'])
307 Out[8]: ['A single string in a list']
307 Out[8]: ['A single string in a list']
308
308
309 In [9]: list_strings(['A','list','of','strings'])
309 In [9]: list_strings(['A','list','of','strings'])
310 Out[9]: ['A', 'list', 'of', 'strings']
310 Out[9]: ['A', 'list', 'of', 'strings']
311 """
311 """
312
312
313 if isinstance(arg, str):
313 if isinstance(arg, str):
314 return [arg]
314 return [arg]
315 else:
315 else:
316 return arg
316 return arg
317
317
318
318
319 def marquee(txt='',width=78,mark='*'):
319 def marquee(txt='',width=78,mark='*'):
320 """Return the input string centered in a 'marquee'.
320 """Return the input string centered in a 'marquee'.
321
321
322 Examples
322 Examples
323 --------
323 --------
324 ::
324 ::
325
325
326 In [16]: marquee('A test',40)
326 In [16]: marquee('A test',40)
327 Out[16]: '**************** A test ****************'
327 Out[16]: '**************** A test ****************'
328
328
329 In [17]: marquee('A test',40,'-')
329 In [17]: marquee('A test',40,'-')
330 Out[17]: '---------------- A test ----------------'
330 Out[17]: '---------------- A test ----------------'
331
331
332 In [18]: marquee('A test',40,' ')
332 In [18]: marquee('A test',40,' ')
333 Out[18]: ' A test '
333 Out[18]: ' A test '
334
334
335 """
335 """
336 if not txt:
336 if not txt:
337 return (mark*width)[:width]
337 return (mark*width)[:width]
338 nmark = (width-len(txt)-2)//len(mark)//2
338 nmark = (width-len(txt)-2)//len(mark)//2
339 if nmark < 0: nmark =0
339 if nmark < 0: nmark =0
340 marks = mark*nmark
340 marks = mark*nmark
341 return '%s %s %s' % (marks,txt,marks)
341 return '%s %s %s' % (marks,txt,marks)
342
342
343
343
344 ini_spaces_re = re.compile(r'^(\s+)')
344 ini_spaces_re = re.compile(r'^(\s+)')
345
345
346 def num_ini_spaces(strng):
346 def num_ini_spaces(strng):
347 """Return the number of initial spaces in a string"""
347 """Return the number of initial spaces in a string"""
348 warnings.warn(
348 warnings.warn(
349 "`num_ini_spaces` is Pending Deprecation since IPython 8.17."
349 "`num_ini_spaces` is Pending Deprecation since IPython 8.17."
350 "It is considered fro removal in in future version. "
350 "It is considered for removal in in future version. "
351 "Please open an issue if you believe it should be kept.",
351 "Please open an issue if you believe it should be kept.",
352 stacklevel=2,
352 stacklevel=2,
353 category=PendingDeprecationWarning,
353 category=PendingDeprecationWarning,
354 )
354 )
355 ini_spaces = ini_spaces_re.match(strng)
355 ini_spaces = ini_spaces_re.match(strng)
356 if ini_spaces:
356 if ini_spaces:
357 return ini_spaces.end()
357 return ini_spaces.end()
358 else:
358 else:
359 return 0
359 return 0
360
360
361
361
362 def format_screen(strng):
362 def format_screen(strng):
363 """Format a string for screen printing.
363 """Format a string for screen printing.
364
364
365 This removes some latex-type format codes."""
365 This removes some latex-type format codes."""
366 # Paragraph continue
366 # Paragraph continue
367 par_re = re.compile(r'\\$',re.MULTILINE)
367 par_re = re.compile(r'\\$',re.MULTILINE)
368 strng = par_re.sub('',strng)
368 strng = par_re.sub('',strng)
369 return strng
369 return strng
370
370
371
371
372 def dedent(text: str) -> str:
372 def dedent(text: str) -> str:
373 """Equivalent of textwrap.dedent that ignores unindented first line.
373 """Equivalent of textwrap.dedent that ignores unindented first line.
374
374
375 This means it will still dedent strings like:
375 This means it will still dedent strings like:
376 '''foo
376 '''foo
377 is a bar
377 is a bar
378 '''
378 '''
379
379
380 For use in wrap_paragraphs.
380 For use in wrap_paragraphs.
381 """
381 """
382
382
383 if text.startswith('\n'):
383 if text.startswith('\n'):
384 # text starts with blank line, don't ignore the first line
384 # text starts with blank line, don't ignore the first line
385 return textwrap.dedent(text)
385 return textwrap.dedent(text)
386
386
387 # split first line
387 # split first line
388 splits = text.split('\n',1)
388 splits = text.split('\n',1)
389 if len(splits) == 1:
389 if len(splits) == 1:
390 # only one line
390 # only one line
391 return textwrap.dedent(text)
391 return textwrap.dedent(text)
392
392
393 first, rest = splits
393 first, rest = splits
394 # dedent everything but the first line
394 # dedent everything but the first line
395 rest = textwrap.dedent(rest)
395 rest = textwrap.dedent(rest)
396 return '\n'.join([first, rest])
396 return '\n'.join([first, rest])
397
397
398
398
399 def wrap_paragraphs(text, ncols=80):
399 def wrap_paragraphs(text, ncols=80):
400 """Wrap multiple paragraphs to fit a specified width.
400 """Wrap multiple paragraphs to fit a specified width.
401
401
402 This is equivalent to textwrap.wrap, but with support for multiple
402 This is equivalent to textwrap.wrap, but with support for multiple
403 paragraphs, as separated by empty lines.
403 paragraphs, as separated by empty lines.
404
404
405 Returns
405 Returns
406 -------
406 -------
407 list of complete paragraphs, wrapped to fill `ncols` columns.
407 list of complete paragraphs, wrapped to fill `ncols` columns.
408 """
408 """
409 warnings.warn(
409 warnings.warn(
410 "`wrap_paragraphs` is Pending Deprecation since IPython 8.17."
410 "`wrap_paragraphs` is Pending Deprecation since IPython 8.17."
411 "It is considered fro removal in in future version. "
411 "It is considered for removal in in future version. "
412 "Please open an issue if you believe it should be kept.",
412 "Please open an issue if you believe it should be kept.",
413 stacklevel=2,
413 stacklevel=2,
414 category=PendingDeprecationWarning,
414 category=PendingDeprecationWarning,
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 strip_email_quotes(text):
431 def strip_email_quotes(text):
432 """Strip leading email quotation characters ('>').
432 """Strip leading email quotation characters ('>').
433
433
434 Removes any combination of leading '>' interspersed with whitespace that
434 Removes any combination of leading '>' interspersed with whitespace that
435 appears *identically* in all lines of the input text.
435 appears *identically* in all lines of the input text.
436
436
437 Parameters
437 Parameters
438 ----------
438 ----------
439 text : str
439 text : str
440
440
441 Examples
441 Examples
442 --------
442 --------
443
443
444 Simple uses::
444 Simple uses::
445
445
446 In [2]: strip_email_quotes('> > text')
446 In [2]: strip_email_quotes('> > text')
447 Out[2]: 'text'
447 Out[2]: 'text'
448
448
449 In [3]: strip_email_quotes('> > text\\n> > more')
449 In [3]: strip_email_quotes('> > text\\n> > more')
450 Out[3]: 'text\\nmore'
450 Out[3]: 'text\\nmore'
451
451
452 Note how only the common prefix that appears in all lines is stripped::
452 Note how only the common prefix that appears in all lines is stripped::
453
453
454 In [4]: strip_email_quotes('> > text\\n> > more\\n> more...')
454 In [4]: strip_email_quotes('> > text\\n> > more\\n> more...')
455 Out[4]: '> text\\n> more\\nmore...'
455 Out[4]: '> text\\n> more\\nmore...'
456
456
457 So if any line has no quote marks ('>'), then none are stripped from any
457 So if any line has no quote marks ('>'), then none are stripped from any
458 of them ::
458 of them ::
459
459
460 In [5]: strip_email_quotes('> > text\\n> > more\\nlast different')
460 In [5]: strip_email_quotes('> > text\\n> > more\\nlast different')
461 Out[5]: '> > text\\n> > more\\nlast different'
461 Out[5]: '> > text\\n> > more\\nlast different'
462 """
462 """
463 lines = text.splitlines()
463 lines = text.splitlines()
464 strip_len = 0
464 strip_len = 0
465
465
466 for characters in zip(*lines):
466 for characters in zip(*lines):
467 # Check if all characters in this position are the same
467 # Check if all characters in this position are the same
468 if len(set(characters)) > 1:
468 if len(set(characters)) > 1:
469 break
469 break
470 prefix_char = characters[0]
470 prefix_char = characters[0]
471
471
472 if prefix_char in string.whitespace or prefix_char == ">":
472 if prefix_char in string.whitespace or prefix_char == ">":
473 strip_len += 1
473 strip_len += 1
474 else:
474 else:
475 break
475 break
476
476
477 text = "\n".join([ln[strip_len:] for ln in lines])
477 text = "\n".join([ln[strip_len:] for ln in lines])
478 return text
478 return text
479
479
480
480
481 def strip_ansi(source):
481 def strip_ansi(source):
482 """
482 """
483 Remove ansi escape codes from text.
483 Remove ansi escape codes from text.
484
484
485 Parameters
485 Parameters
486 ----------
486 ----------
487 source : str
487 source : str
488 Source to remove the ansi from
488 Source to remove the ansi from
489 """
489 """
490 warnings.warn(
490 warnings.warn(
491 "`strip_ansi` is Pending Deprecation since IPython 8.17."
491 "`strip_ansi` is Pending Deprecation since IPython 8.17."
492 "It is considered fro removal in in future version. "
492 "It is considered for removal in in future version. "
493 "Please open an issue if you believe it should be kept.",
493 "Please open an issue if you believe it should be kept.",
494 stacklevel=2,
494 stacklevel=2,
495 category=PendingDeprecationWarning,
495 category=PendingDeprecationWarning,
496 )
496 )
497
497
498 return re.sub(r'\033\[(\d|;)+?m', '', source)
498 return re.sub(r'\033\[(\d|;)+?m', '', source)
499
499
500
500
501 class EvalFormatter(Formatter):
501 class EvalFormatter(Formatter):
502 """A String Formatter that allows evaluation of simple expressions.
502 """A String Formatter that allows evaluation of simple expressions.
503
503
504 Note that this version interprets a `:` as specifying a format string (as per
504 Note that this version interprets a `:` as specifying a format string (as per
505 standard string formatting), so if slicing is required, you must explicitly
505 standard string formatting), so if slicing is required, you must explicitly
506 create a slice.
506 create a slice.
507
507
508 This is to be used in templating cases, such as the parallel batch
508 This is to be used in templating cases, such as the parallel batch
509 script templates, where simple arithmetic on arguments is useful.
509 script templates, where simple arithmetic on arguments is useful.
510
510
511 Examples
511 Examples
512 --------
512 --------
513 ::
513 ::
514
514
515 In [1]: f = EvalFormatter()
515 In [1]: f = EvalFormatter()
516 In [2]: f.format('{n//4}', n=8)
516 In [2]: f.format('{n//4}', n=8)
517 Out[2]: '2'
517 Out[2]: '2'
518
518
519 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
519 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
520 Out[3]: 'll'
520 Out[3]: 'll'
521 """
521 """
522 def get_field(self, name, args, kwargs):
522 def get_field(self, name, args, kwargs):
523 v = eval(name, kwargs)
523 v = eval(name, kwargs)
524 return v, name
524 return v, name
525
525
526 #XXX: As of Python 3.4, the format string parsing no longer splits on a colon
526 #XXX: As of Python 3.4, the format string parsing no longer splits on a colon
527 # inside [], so EvalFormatter can handle slicing. Once we only support 3.4 and
527 # inside [], so EvalFormatter can handle slicing. Once we only support 3.4 and
528 # above, it should be possible to remove FullEvalFormatter.
528 # above, it should be possible to remove FullEvalFormatter.
529
529
530 class FullEvalFormatter(Formatter):
530 class FullEvalFormatter(Formatter):
531 """A String Formatter that allows evaluation of simple expressions.
531 """A String Formatter that allows evaluation of simple expressions.
532
532
533 Any time a format key is not found in the kwargs,
533 Any time a format key is not found in the kwargs,
534 it will be tried as an expression in the kwargs namespace.
534 it will be tried as an expression in the kwargs namespace.
535
535
536 Note that this version allows slicing using [1:2], so you cannot specify
536 Note that this version allows slicing using [1:2], so you cannot specify
537 a format string. Use :class:`EvalFormatter` to permit format strings.
537 a format string. Use :class:`EvalFormatter` to permit format strings.
538
538
539 Examples
539 Examples
540 --------
540 --------
541 ::
541 ::
542
542
543 In [1]: f = FullEvalFormatter()
543 In [1]: f = FullEvalFormatter()
544 In [2]: f.format('{n//4}', n=8)
544 In [2]: f.format('{n//4}', n=8)
545 Out[2]: '2'
545 Out[2]: '2'
546
546
547 In [3]: f.format('{list(range(5))[2:4]}')
547 In [3]: f.format('{list(range(5))[2:4]}')
548 Out[3]: '[2, 3]'
548 Out[3]: '[2, 3]'
549
549
550 In [4]: f.format('{3*2}')
550 In [4]: f.format('{3*2}')
551 Out[4]: '6'
551 Out[4]: '6'
552 """
552 """
553 # copied from Formatter._vformat with minor changes to allow eval
553 # copied from Formatter._vformat with minor changes to allow eval
554 # and replace the format_spec code with slicing
554 # and replace the format_spec code with slicing
555 def vformat(
555 def vformat(
556 self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any]
556 self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any]
557 ) -> str:
557 ) -> str:
558 result = []
558 result = []
559 conversion: Optional[str]
559 conversion: Optional[str]
560 for literal_text, field_name, format_spec, conversion in self.parse(
560 for literal_text, field_name, format_spec, conversion in self.parse(
561 format_string
561 format_string
562 ):
562 ):
563 # output the literal text
563 # output the literal text
564 if literal_text:
564 if literal_text:
565 result.append(literal_text)
565 result.append(literal_text)
566
566
567 # if there's a field, output it
567 # if there's a field, output it
568 if field_name is not None:
568 if field_name is not None:
569 # this is some markup, find the object and do
569 # this is some markup, find the object and do
570 # the formatting
570 # the formatting
571
571
572 if format_spec:
572 if format_spec:
573 # override format spec, to allow slicing:
573 # override format spec, to allow slicing:
574 field_name = ':'.join([field_name, format_spec])
574 field_name = ':'.join([field_name, format_spec])
575
575
576 # eval the contents of the field for the object
576 # eval the contents of the field for the object
577 # to be formatted
577 # to be formatted
578 obj = eval(field_name, dict(kwargs))
578 obj = eval(field_name, dict(kwargs))
579
579
580 # do any conversion on the resulting object
580 # do any conversion on the resulting object
581 # type issue in typeshed, fined in https://github.com/python/typeshed/pull/11377
581 # type issue in typeshed, fined in https://github.com/python/typeshed/pull/11377
582 obj = self.convert_field(obj, conversion) # type: ignore[arg-type]
582 obj = self.convert_field(obj, conversion) # type: ignore[arg-type]
583
583
584 # format the object and append to the result
584 # format the object and append to the result
585 result.append(self.format_field(obj, ''))
585 result.append(self.format_field(obj, ''))
586
586
587 return ''.join(result)
587 return ''.join(result)
588
588
589
589
590 class DollarFormatter(FullEvalFormatter):
590 class DollarFormatter(FullEvalFormatter):
591 """Formatter allowing Itpl style $foo replacement, for names and attribute
591 """Formatter allowing Itpl style $foo replacement, for names and attribute
592 access only. Standard {foo} replacement also works, and allows full
592 access only. Standard {foo} replacement also works, and allows full
593 evaluation of its arguments.
593 evaluation of its arguments.
594
594
595 Examples
595 Examples
596 --------
596 --------
597 ::
597 ::
598
598
599 In [1]: f = DollarFormatter()
599 In [1]: f = DollarFormatter()
600 In [2]: f.format('{n//4}', n=8)
600 In [2]: f.format('{n//4}', n=8)
601 Out[2]: '2'
601 Out[2]: '2'
602
602
603 In [3]: f.format('23 * 76 is $result', result=23*76)
603 In [3]: f.format('23 * 76 is $result', result=23*76)
604 Out[3]: '23 * 76 is 1748'
604 Out[3]: '23 * 76 is 1748'
605
605
606 In [4]: f.format('$a or {b}', a=1, b=2)
606 In [4]: f.format('$a or {b}', a=1, b=2)
607 Out[4]: '1 or 2'
607 Out[4]: '1 or 2'
608 """
608 """
609 _dollar_pattern_ignore_single_quote = re.compile(r"(.*?)\$(\$?[\w\.]+)(?=([^']*'[^']*')*[^']*$)")
609 _dollar_pattern_ignore_single_quote = re.compile(r"(.*?)\$(\$?[\w\.]+)(?=([^']*'[^']*')*[^']*$)")
610 def parse(self, fmt_string):
610 def parse(self, fmt_string):
611 for literal_txt, field_name, format_spec, conversion \
611 for literal_txt, field_name, format_spec, conversion \
612 in Formatter.parse(self, fmt_string):
612 in Formatter.parse(self, fmt_string):
613
613
614 # Find $foo patterns in the literal text.
614 # Find $foo patterns in the literal text.
615 continue_from = 0
615 continue_from = 0
616 txt = ""
616 txt = ""
617 for m in self._dollar_pattern_ignore_single_quote.finditer(literal_txt):
617 for m in self._dollar_pattern_ignore_single_quote.finditer(literal_txt):
618 new_txt, new_field = m.group(1,2)
618 new_txt, new_field = m.group(1,2)
619 # $$foo --> $foo
619 # $$foo --> $foo
620 if new_field.startswith("$"):
620 if new_field.startswith("$"):
621 txt += new_txt + new_field
621 txt += new_txt + new_field
622 else:
622 else:
623 yield (txt + new_txt, new_field, "", None)
623 yield (txt + new_txt, new_field, "", None)
624 txt = ""
624 txt = ""
625 continue_from = m.end()
625 continue_from = m.end()
626
626
627 # Re-yield the {foo} style pattern
627 # Re-yield the {foo} style pattern
628 yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
628 yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
629
629
630 def __repr__(self):
630 def __repr__(self):
631 return "<DollarFormatter>"
631 return "<DollarFormatter>"
632
632
633 #-----------------------------------------------------------------------------
633 #-----------------------------------------------------------------------------
634 # Utils to columnize a list of string
634 # Utils to columnize a list of string
635 #-----------------------------------------------------------------------------
635 #-----------------------------------------------------------------------------
636
636
637 def _col_chunks(l, max_rows, row_first=False):
637 def _col_chunks(l, max_rows, row_first=False):
638 """Yield successive max_rows-sized column chunks from l."""
638 """Yield successive max_rows-sized column chunks from l."""
639 if row_first:
639 if row_first:
640 ncols = (len(l) // max_rows) + (len(l) % max_rows > 0)
640 ncols = (len(l) // max_rows) + (len(l) % max_rows > 0)
641 for i in range(ncols):
641 for i in range(ncols):
642 yield [l[j] for j in range(i, len(l), ncols)]
642 yield [l[j] for j in range(i, len(l), ncols)]
643 else:
643 else:
644 for i in range(0, len(l), max_rows):
644 for i in range(0, len(l), max_rows):
645 yield l[i:(i + max_rows)]
645 yield l[i:(i + max_rows)]
646
646
647
647
648 def _find_optimal(
648 def _find_optimal(
649 rlist: List[str], row_first: bool, separator_size: int, displaywidth: int
649 rlist: List[str], row_first: bool, separator_size: int, displaywidth: int
650 ) -> Dict[str, Any]:
650 ) -> Dict[str, Any]:
651 """Calculate optimal info to columnize a list of string"""
651 """Calculate optimal info to columnize a list of string"""
652 for max_rows in range(1, len(rlist) + 1):
652 for max_rows in range(1, len(rlist) + 1):
653 col_widths = list(map(max, _col_chunks(rlist, max_rows, row_first)))
653 col_widths = list(map(max, _col_chunks(rlist, max_rows, row_first)))
654 sumlength = sum(col_widths)
654 sumlength = sum(col_widths)
655 ncols = len(col_widths)
655 ncols = len(col_widths)
656 if sumlength + separator_size * (ncols - 1) <= displaywidth:
656 if sumlength + separator_size * (ncols - 1) <= displaywidth:
657 break
657 break
658 return {'num_columns': ncols,
658 return {'num_columns': ncols,
659 'optimal_separator_width': (displaywidth - sumlength) // (ncols - 1) if (ncols - 1) else 0,
659 'optimal_separator_width': (displaywidth - sumlength) // (ncols - 1) if (ncols - 1) else 0,
660 'max_rows': max_rows,
660 'max_rows': max_rows,
661 'column_widths': col_widths
661 'column_widths': col_widths
662 }
662 }
663
663
664
664
665 def _get_or_default(mylist, i, default=None):
665 def _get_or_default(mylist, i, default=None):
666 """return list item number, or default if don't exist"""
666 """return list item number, or default if don't exist"""
667 if i >= len(mylist):
667 if i >= len(mylist):
668 return default
668 return default
669 else :
669 else :
670 return mylist[i]
670 return mylist[i]
671
671
672
672
673 def compute_item_matrix(
673 def compute_item_matrix(
674 items: List[str],
674 items: List[str],
675 row_first: bool = False,
675 row_first: bool = False,
676 empty: Optional[str] = None,
676 empty: Optional[str] = None,
677 *,
677 *,
678 separator_size: int = 2,
678 separator_size: int = 2,
679 displaywidth: int = 80,
679 displaywidth: int = 80,
680 ) -> Tuple[List[List[int]], Dict[str, int]]:
680 ) -> Tuple[List[List[int]], Dict[str, int]]:
681 """Returns a nested list, and info to columnize items
681 """Returns a nested list, and info to columnize items
682
682
683 Parameters
683 Parameters
684 ----------
684 ----------
685 items
685 items
686 list of strings to columize
686 list of strings to columize
687 row_first : (default False)
687 row_first : (default False)
688 Whether to compute columns for a row-first matrix instead of
688 Whether to compute columns for a row-first matrix instead of
689 column-first (default).
689 column-first (default).
690 empty : (default None)
690 empty : (default None)
691 default value to fill list if needed
691 default value to fill list if needed
692 separator_size : int (default=2)
692 separator_size : int (default=2)
693 How much characters will be used as a separation between each columns.
693 How much characters will be used as a separation between each columns.
694 displaywidth : int (default=80)
694 displaywidth : int (default=80)
695 The width of the area onto which the columns should enter
695 The width of the area onto which the columns should enter
696
696
697 Returns
697 Returns
698 -------
698 -------
699 strings_matrix
699 strings_matrix
700 nested list of string, the outer most list contains as many list as
700 nested list of string, the outer most list contains as many list as
701 rows, the innermost lists have each as many element as columns. If the
701 rows, the innermost lists have each as many element as columns. If the
702 total number of elements in `items` does not equal the product of
702 total number of elements in `items` does not equal the product of
703 rows*columns, the last element of some lists are filled with `None`.
703 rows*columns, the last element of some lists are filled with `None`.
704 dict_info
704 dict_info
705 some info to make columnize easier:
705 some info to make columnize easier:
706
706
707 num_columns
707 num_columns
708 number of columns
708 number of columns
709 max_rows
709 max_rows
710 maximum number of rows (final number may be less)
710 maximum number of rows (final number may be less)
711 column_widths
711 column_widths
712 list of with of each columns
712 list of with of each columns
713 optimal_separator_width
713 optimal_separator_width
714 best separator width between columns
714 best separator width between columns
715
715
716 Examples
716 Examples
717 --------
717 --------
718 ::
718 ::
719
719
720 In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l']
720 In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l']
721 In [2]: list, info = compute_item_matrix(l, displaywidth=12)
721 In [2]: list, info = compute_item_matrix(l, displaywidth=12)
722 In [3]: list
722 In [3]: list
723 Out[3]: [['aaa', 'f', 'k'], ['b', 'g', 'l'], ['cc', 'h', None], ['d', 'i', None], ['eeeee', 'j', None]]
723 Out[3]: [['aaa', 'f', 'k'], ['b', 'g', 'l'], ['cc', 'h', None], ['d', 'i', None], ['eeeee', 'j', None]]
724 In [4]: ideal = {'num_columns': 3, 'column_widths': [5, 1, 1], 'optimal_separator_width': 2, 'max_rows': 5}
724 In [4]: ideal = {'num_columns': 3, 'column_widths': [5, 1, 1], 'optimal_separator_width': 2, 'max_rows': 5}
725 In [5]: all((info[k] == ideal[k] for k in ideal.keys()))
725 In [5]: all((info[k] == ideal[k] for k in ideal.keys()))
726 Out[5]: True
726 Out[5]: True
727 """
727 """
728 warnings.warn(
728 warnings.warn(
729 "`compute_item_matrix` is Pending Deprecation since IPython 8.17."
729 "`compute_item_matrix` is Pending Deprecation since IPython 8.17."
730 "It is considered fro removal in in future version. "
730 "It is considered for removal in in future version. "
731 "Please open an issue if you believe it should be kept.",
731 "Please open an issue if you believe it should be kept.",
732 stacklevel=2,
732 stacklevel=2,
733 category=PendingDeprecationWarning,
733 category=PendingDeprecationWarning,
734 )
734 )
735 info = _find_optimal(
735 info = _find_optimal(
736 list(map(len, items)), # type: ignore[arg-type]
736 list(map(len, items)), # type: ignore[arg-type]
737 row_first,
737 row_first,
738 separator_size=separator_size,
738 separator_size=separator_size,
739 displaywidth=displaywidth,
739 displaywidth=displaywidth,
740 )
740 )
741 nrow, ncol = info["max_rows"], info["num_columns"]
741 nrow, ncol = info["max_rows"], info["num_columns"]
742 if row_first:
742 if row_first:
743 return ([[_get_or_default(items, r * ncol + c, default=empty) for c in range(ncol)] for r in range(nrow)], info)
743 return ([[_get_or_default(items, r * ncol + c, default=empty) for c in range(ncol)] for r in range(nrow)], info)
744 else:
744 else:
745 return ([[_get_or_default(items, c * nrow + r, default=empty) for c in range(ncol)] for r in range(nrow)], info)
745 return ([[_get_or_default(items, c * nrow + r, default=empty) for c in range(ncol)] for r in range(nrow)], info)
746
746
747
747
748 def columnize(
748 def columnize(
749 items: List[str],
749 items: List[str],
750 row_first: bool = False,
750 row_first: bool = False,
751 separator: str = " ",
751 separator: str = " ",
752 displaywidth: int = 80,
752 displaywidth: int = 80,
753 spread: bool = False,
753 spread: bool = False,
754 ) -> str:
754 ) -> str:
755 """Transform a list of strings into a single string with columns.
755 """Transform a list of strings into a single string with columns.
756
756
757 Parameters
757 Parameters
758 ----------
758 ----------
759 items : sequence of strings
759 items : sequence of strings
760 The strings to process.
760 The strings to process.
761 row_first : (default False)
761 row_first : (default False)
762 Whether to compute columns for a row-first matrix instead of
762 Whether to compute columns for a row-first matrix instead of
763 column-first (default).
763 column-first (default).
764 separator : str, optional [default is two spaces]
764 separator : str, optional [default is two spaces]
765 The string that separates columns.
765 The string that separates columns.
766 displaywidth : int, optional [default is 80]
766 displaywidth : int, optional [default is 80]
767 Width of the display in number of characters.
767 Width of the display in number of characters.
768
768
769 Returns
769 Returns
770 -------
770 -------
771 The formatted string.
771 The formatted string.
772 """
772 """
773 warnings.warn(
773 warnings.warn(
774 "`columnize` is Pending Deprecation since IPython 8.17."
774 "`columnize` is Pending Deprecation since IPython 8.17."
775 "It is considered for removal in future versions. "
775 "It is considered for removal in future versions. "
776 "Please open an issue if you believe it should be kept.",
776 "Please open an issue if you believe it should be kept.",
777 stacklevel=2,
777 stacklevel=2,
778 category=PendingDeprecationWarning,
778 category=PendingDeprecationWarning,
779 )
779 )
780 if not items:
780 if not items:
781 return "\n"
781 return "\n"
782 matrix: List[List[int]]
782 matrix: List[List[int]]
783 matrix, info = compute_item_matrix(
783 matrix, info = compute_item_matrix(
784 items,
784 items,
785 row_first=row_first,
785 row_first=row_first,
786 separator_size=len(separator),
786 separator_size=len(separator),
787 displaywidth=displaywidth,
787 displaywidth=displaywidth,
788 )
788 )
789 if spread:
789 if spread:
790 separator = separator.ljust(int(info["optimal_separator_width"]))
790 separator = separator.ljust(int(info["optimal_separator_width"]))
791 fmatrix: List[filter[int]] = [filter(None, x) for x in matrix]
791 fmatrix: List[filter[int]] = [filter(None, x) for x in matrix]
792 sjoin = lambda x: separator.join(
792 sjoin = lambda x: separator.join(
793 [y.ljust(w, " ") for y, w in zip(x, cast(List[int], info["column_widths"]))]
793 [y.ljust(w, " ") for y, w in zip(x, cast(List[int], info["column_widths"]))]
794 )
794 )
795 return "\n".join(map(sjoin, fmatrix)) + "\n"
795 return "\n".join(map(sjoin, fmatrix)) + "\n"
796
796
797
797
798 def get_text_list(list_, last_sep=' and ', sep=", ", wrap_item_with=""):
798 def get_text_list(list_, last_sep=' and ', sep=", ", wrap_item_with=""):
799 """
799 """
800 Return a string with a natural enumeration of items
800 Return a string with a natural enumeration of items
801
801
802 >>> get_text_list(['a', 'b', 'c', 'd'])
802 >>> get_text_list(['a', 'b', 'c', 'd'])
803 'a, b, c and d'
803 'a, b, c and d'
804 >>> get_text_list(['a', 'b', 'c'], ' or ')
804 >>> get_text_list(['a', 'b', 'c'], ' or ')
805 'a, b or c'
805 'a, b or c'
806 >>> get_text_list(['a', 'b', 'c'], ', ')
806 >>> get_text_list(['a', 'b', 'c'], ', ')
807 'a, b, c'
807 'a, b, c'
808 >>> get_text_list(['a', 'b'], ' or ')
808 >>> get_text_list(['a', 'b'], ' or ')
809 'a or b'
809 'a or b'
810 >>> get_text_list(['a'])
810 >>> get_text_list(['a'])
811 'a'
811 'a'
812 >>> get_text_list([])
812 >>> get_text_list([])
813 ''
813 ''
814 >>> get_text_list(['a', 'b'], wrap_item_with="`")
814 >>> get_text_list(['a', 'b'], wrap_item_with="`")
815 '`a` and `b`'
815 '`a` and `b`'
816 >>> get_text_list(['a', 'b', 'c', 'd'], " = ", sep=" + ")
816 >>> get_text_list(['a', 'b', 'c', 'd'], " = ", sep=" + ")
817 'a + b + c = d'
817 'a + b + c = d'
818 """
818 """
819 if len(list_) == 0:
819 if len(list_) == 0:
820 return ''
820 return ''
821 if wrap_item_with:
821 if wrap_item_with:
822 list_ = ['%s%s%s' % (wrap_item_with, item, wrap_item_with) for
822 list_ = ['%s%s%s' % (wrap_item_with, item, wrap_item_with) for
823 item in list_]
823 item in list_]
824 if len(list_) == 1:
824 if len(list_) == 1:
825 return list_[0]
825 return list_[0]
826 return '%s%s%s' % (
826 return '%s%s%s' % (
827 sep.join(i for i in list_[:-1]),
827 sep.join(i for i in list_[:-1]),
828 last_sep, list_[-1])
828 last_sep, list_[-1])
General Comments 0
You need to be logged in to leave comments. Login now