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