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