text.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. """
  2. Utilities for working with strings and text.
  3. Inheritance diagram:
  4. .. inheritance-diagram:: IPython.utils.text
  5. :parts: 3
  6. """
  7. import os
  8. import re
  9. import string
  10. import sys
  11. import textwrap
  12. import warnings
  13. from string import Formatter
  14. from pathlib import Path
  15. from typing import (
  16. List,
  17. Dict,
  18. Tuple,
  19. Optional,
  20. cast,
  21. Any,
  22. Union,
  23. TypeVar,
  24. )
  25. from collections.abc import Sequence, Mapping, Callable, Iterator
  26. from typing import Self
  27. class LSString(str):
  28. """String derivative with a special access attributes.
  29. These are normal strings, but with the special attributes:
  30. .l (or .list) : value as list (split on newlines).
  31. .n (or .nlstr): original value (the string itself).
  32. .s (or .spstr): value as whitespace-separated string.
  33. .p (or .paths): list of path objects (requires path.py package)
  34. Any values which require transformations are computed only once and
  35. cached.
  36. Such strings are very useful to efficiently interact with the shell, which
  37. typically only understands whitespace-separated options for commands."""
  38. __list: List[str]
  39. __spstr: str
  40. __paths: List[Path]
  41. def get_list(self) -> List[str]:
  42. try:
  43. return self.__list
  44. except AttributeError:
  45. self.__list = self.split('\n')
  46. return self.__list
  47. l = list = property(get_list)
  48. def get_spstr(self) -> str:
  49. try:
  50. return self.__spstr
  51. except AttributeError:
  52. self.__spstr = self.replace('\n',' ')
  53. return self.__spstr
  54. s = spstr = property(get_spstr)
  55. def get_nlstr(self) -> Self:
  56. return self
  57. n = nlstr = property(get_nlstr)
  58. def get_paths(self) -> List[Path]:
  59. try:
  60. return self.__paths
  61. except AttributeError:
  62. self.__paths = [Path(p) for p in self.split('\n') if os.path.exists(p)]
  63. return self.__paths
  64. p = paths = property(get_paths)
  65. # FIXME: We need to reimplement type specific displayhook and then add this
  66. # back as a custom printer. This should also be moved outside utils into the
  67. # core.
  68. # def print_lsstring(arg):
  69. # """ Prettier (non-repr-like) and more informative printer for LSString """
  70. # print("LSString (.p, .n, .l, .s available). Value:")
  71. # print(arg)
  72. #
  73. #
  74. # print_lsstring = result_display.register(LSString)(print_lsstring)
  75. class SList(list[Any]):
  76. """List derivative with a special access attributes.
  77. These are normal lists, but with the special attributes:
  78. * .l (or .list) : value as list (the list itself).
  79. * .n (or .nlstr): value as a string, joined on newlines.
  80. * .s (or .spstr): value as a string, joined on spaces.
  81. * .p (or .paths): list of path objects (requires path.py package)
  82. Any values which require transformations are computed only once and
  83. cached."""
  84. __spstr: str
  85. __nlstr: str
  86. __paths: List[Path]
  87. def get_list(self) -> Self:
  88. return self
  89. l = list = property(get_list)
  90. def get_spstr(self) -> str:
  91. try:
  92. return self.__spstr
  93. except AttributeError:
  94. self.__spstr = ' '.join(self)
  95. return self.__spstr
  96. s = spstr = property(get_spstr)
  97. def get_nlstr(self) -> str:
  98. try:
  99. return self.__nlstr
  100. except AttributeError:
  101. self.__nlstr = '\n'.join(self)
  102. return self.__nlstr
  103. n = nlstr = property(get_nlstr)
  104. def get_paths(self) -> List[Path]:
  105. try:
  106. return self.__paths
  107. except AttributeError:
  108. self.__paths = [Path(p) for p in self if os.path.exists(p)]
  109. return self.__paths
  110. p = paths = property(get_paths)
  111. def grep(
  112. self,
  113. pattern: Union[str, Callable[[Any], re.Match[str] | None]],
  114. prune: bool = False,
  115. field: Optional[int] = None,
  116. ) -> Self:
  117. """Return all strings matching 'pattern' (a regex or callable)
  118. This is case-insensitive. If prune is true, return all items
  119. NOT matching the pattern.
  120. If field is specified, the match must occur in the specified
  121. whitespace-separated field.
  122. Examples::
  123. a.grep( lambda x: x.startswith('C') )
  124. a.grep('Cha.*log', prune=1)
  125. a.grep('chm', field=-1)
  126. """
  127. def match_target(s: str) -> str:
  128. if field is None:
  129. return s
  130. parts = s.split()
  131. try:
  132. tgt = parts[field]
  133. return tgt
  134. except IndexError:
  135. return ""
  136. if isinstance(pattern, str):
  137. pred = lambda x : re.search(pattern, x, re.IGNORECASE)
  138. else:
  139. pred = pattern
  140. if not prune:
  141. return type(self)([el for el in self if pred(match_target(el))]) # type: ignore [no-untyped-call]
  142. else:
  143. return type(self)([el for el in self if not pred(match_target(el))]) # type: ignore [no-untyped-call]
  144. def fields(self, *fields: List[str]) -> List[List[str]]:
  145. """Collect whitespace-separated fields from string list
  146. Allows quick awk-like usage of string lists.
  147. Example data (in var a, created by 'a = !ls -l')::
  148. -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
  149. drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
  150. * ``a.fields(0)`` is ``['-rwxrwxrwx', 'drwxrwxrwx+']``
  151. * ``a.fields(1,0)`` is ``['1 -rwxrwxrwx', '6 drwxrwxrwx+']``
  152. (note the joining by space).
  153. * ``a.fields(-1)`` is ``['ChangeLog', 'IPython']``
  154. IndexErrors are ignored.
  155. Without args, fields() just split()'s the strings.
  156. """
  157. if len(fields) == 0:
  158. return [el.split() for el in self]
  159. res = SList()
  160. for el in [f.split() for f in self]:
  161. lineparts = []
  162. for fd in fields:
  163. try:
  164. lineparts.append(el[fd])
  165. except IndexError:
  166. pass
  167. if lineparts:
  168. res.append(" ".join(lineparts))
  169. return res
  170. def sort( # type:ignore[override]
  171. self,
  172. field: Optional[List[str]] = None,
  173. nums: bool = False,
  174. ) -> Self:
  175. """sort by specified fields (see fields())
  176. Example::
  177. a.sort(1, nums = True)
  178. Sorts a by second field, in numerical order (so that 21 > 3)
  179. """
  180. #decorate, sort, undecorate
  181. if field is not None:
  182. dsu = [[SList([line]).fields(field), line] for line in self]
  183. else:
  184. dsu = [[line, line] for line in self]
  185. if nums:
  186. for i in range(len(dsu)):
  187. numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
  188. try:
  189. n = int(numstr)
  190. except ValueError:
  191. n = 0
  192. dsu[i][0] = n
  193. dsu.sort()
  194. return type(self)([t[1] for t in dsu])
  195. def indent(instr: str, nspaces: int = 4, ntabs: int = 0, flatten: bool = False) -> str:
  196. """Indent a string a given number of spaces or tabstops.
  197. indent(str, nspaces=4, ntabs=0) -> indent str by ntabs+nspaces.
  198. Parameters
  199. ----------
  200. instr : basestring
  201. The string to be indented.
  202. nspaces : int (default: 4)
  203. The number of spaces to be indented.
  204. ntabs : int (default: 0)
  205. The number of tabs to be indented.
  206. flatten : bool (default: False)
  207. Whether to scrub existing indentation. If True, all lines will be
  208. aligned to the same indentation. If False, existing indentation will
  209. be strictly increased.
  210. Returns
  211. -------
  212. str : string indented by ntabs and nspaces.
  213. """
  214. ind = "\t" * ntabs + " " * nspaces
  215. if flatten:
  216. pat = re.compile(r'^\s*', re.MULTILINE)
  217. else:
  218. pat = re.compile(r'^', re.MULTILINE)
  219. outstr = re.sub(pat, ind, instr)
  220. if outstr.endswith(os.linesep+ind):
  221. return outstr[:-len(ind)]
  222. else:
  223. return outstr
  224. def list_strings(arg: Union[str, List[str]]) -> List[str]:
  225. """Always return a list of strings, given a string or list of strings
  226. as input.
  227. Examples
  228. --------
  229. ::
  230. In [7]: list_strings('A single string')
  231. Out[7]: ['A single string']
  232. In [8]: list_strings(['A single string in a list'])
  233. Out[8]: ['A single string in a list']
  234. In [9]: list_strings(['A','list','of','strings'])
  235. Out[9]: ['A', 'list', 'of', 'strings']
  236. """
  237. if isinstance(arg, str):
  238. return [arg]
  239. else:
  240. return arg
  241. def marquee(txt: str = "", width: int = 78, mark: str = "*") -> str:
  242. """Return the input string centered in a 'marquee'.
  243. Examples
  244. --------
  245. ::
  246. In [16]: marquee('A test',40)
  247. Out[16]: '**************** A test ****************'
  248. In [17]: marquee('A test',40,'-')
  249. Out[17]: '---------------- A test ----------------'
  250. In [18]: marquee('A test',40,' ')
  251. Out[18]: ' A test '
  252. """
  253. if not txt:
  254. return (mark*width)[:width]
  255. nmark = (width-len(txt)-2)//len(mark)//2
  256. if nmark < 0: nmark =0
  257. marks = mark*nmark
  258. return '%s %s %s' % (marks,txt,marks)
  259. def format_screen(strng: str) -> str:
  260. """Format a string for screen printing.
  261. This removes some latex-type format codes."""
  262. # Paragraph continue
  263. par_re = re.compile(r'\\$',re.MULTILINE)
  264. strng = par_re.sub('',strng)
  265. return strng
  266. def dedent(text: str) -> str:
  267. """Equivalent of textwrap.dedent that ignores unindented first line.
  268. This means it will still dedent strings like:
  269. '''foo
  270. is a bar
  271. '''
  272. For use in wrap_paragraphs.
  273. """
  274. if text.startswith('\n'):
  275. # text starts with blank line, don't ignore the first line
  276. return textwrap.dedent(text)
  277. # split first line
  278. splits = text.split('\n',1)
  279. if len(splits) == 1:
  280. # only one line
  281. return textwrap.dedent(text)
  282. first, rest = splits
  283. # dedent everything but the first line
  284. rest = textwrap.dedent(rest)
  285. return '\n'.join([first, rest])
  286. def strip_email_quotes(text: str) -> str:
  287. """Strip leading email quotation characters ('>').
  288. Removes any combination of leading '>' interspersed with whitespace that
  289. appears *identically* in all lines of the input text.
  290. Parameters
  291. ----------
  292. text : str
  293. Examples
  294. --------
  295. Simple uses::
  296. In [2]: strip_email_quotes('> > text')
  297. Out[2]: 'text'
  298. In [3]: strip_email_quotes('> > text\\n> > more')
  299. Out[3]: 'text\\nmore'
  300. Note how only the common prefix that appears in all lines is stripped::
  301. In [4]: strip_email_quotes('> > text\\n> > more\\n> more...')
  302. Out[4]: '> text\\n> more\\nmore...'
  303. So if any line has no quote marks ('>'), then none are stripped from any
  304. of them ::
  305. In [5]: strip_email_quotes('> > text\\n> > more\\nlast different')
  306. Out[5]: '> > text\\n> > more\\nlast different'
  307. """
  308. lines = text.splitlines()
  309. strip_len = 0
  310. for characters in zip(*lines):
  311. # Check if all characters in this position are the same
  312. if len(set(characters)) > 1:
  313. break
  314. prefix_char = characters[0]
  315. if prefix_char in string.whitespace or prefix_char == ">":
  316. strip_len += 1
  317. else:
  318. break
  319. text = "\n".join([ln[strip_len:] for ln in lines])
  320. return text
  321. class EvalFormatter(Formatter):
  322. """A String Formatter that allows evaluation of simple expressions.
  323. Note that this version interprets a `:` as specifying a format string (as per
  324. standard string formatting), so if slicing is required, you must explicitly
  325. create a slice.
  326. Note that on Python 3.14+ this version interprets `[]` as indexing operator
  327. so you need to use generators instead of list comprehensions, for example:
  328. `list(i for i in range(10))`.
  329. This is to be used in templating cases, such as the parallel batch
  330. script templates, where simple arithmetic on arguments is useful.
  331. Examples
  332. --------
  333. ::
  334. In [1]: f = EvalFormatter()
  335. In [2]: f.format('{n//4}', n=8)
  336. Out[2]: '2'
  337. In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
  338. Out[3]: 'll'
  339. """
  340. def get_field(self, name: str, args: Any, kwargs: Any) -> Tuple[Any, str]:
  341. v = eval(name, kwargs, kwargs)
  342. return v, name
  343. #XXX: As of Python 3.4, the format string parsing no longer splits on a colon
  344. # inside [], so EvalFormatter can handle slicing. Once we only support 3.4 and
  345. # above, it should be possible to remove FullEvalFormatter.
  346. class FullEvalFormatter(Formatter):
  347. """A String Formatter that allows evaluation of simple expressions.
  348. Any time a format key is not found in the kwargs,
  349. it will be tried as an expression in the kwargs namespace.
  350. Note that this version allows slicing using [1:2], so you cannot specify
  351. a format string. Use :class:`EvalFormatter` to permit format strings.
  352. Examples
  353. --------
  354. ::
  355. In [1]: f = FullEvalFormatter()
  356. In [2]: f.format('{n//4}', n=8)
  357. Out[2]: '2'
  358. In [3]: f.format('{list(range(5))[2:4]}')
  359. Out[3]: '[2, 3]'
  360. In [4]: f.format('{3*2}')
  361. Out[4]: '6'
  362. """
  363. # copied from Formatter._vformat with minor changes to allow eval
  364. # and replace the format_spec code with slicing
  365. def vformat(
  366. self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any]
  367. ) -> str:
  368. result = []
  369. conversion: Optional[str]
  370. for literal_text, field_name, format_spec, conversion in self.parse(
  371. format_string
  372. ):
  373. # output the literal text
  374. if literal_text:
  375. result.append(literal_text)
  376. # if there's a field, output it
  377. if field_name is not None:
  378. # this is some markup, find the object and do
  379. # the formatting
  380. if format_spec:
  381. # override format spec, to allow slicing:
  382. field_name = ':'.join([field_name, format_spec])
  383. # eval the contents of the field for the object
  384. # to be formatted
  385. obj = eval(field_name, dict(kwargs))
  386. # do any conversion on the resulting object
  387. # type issue in typeshed, fined in https://github.com/python/typeshed/pull/11377
  388. obj = self.convert_field(obj, conversion)
  389. # format the object and append to the result
  390. result.append(self.format_field(obj, ''))
  391. return ''.join(result)
  392. class DollarFormatter(FullEvalFormatter):
  393. """Formatter allowing Itpl style $foo replacement, for names and attribute
  394. access only. Standard {foo} replacement also works, and allows full
  395. evaluation of its arguments.
  396. Examples
  397. --------
  398. ::
  399. In [1]: f = DollarFormatter()
  400. In [2]: f.format('{n//4}', n=8)
  401. Out[2]: '2'
  402. In [3]: f.format('23 * 76 is $result', result=23*76)
  403. Out[3]: '23 * 76 is 1748'
  404. In [4]: f.format('$a or {b}', a=1, b=2)
  405. Out[4]: '1 or 2'
  406. """
  407. _dollar_pattern_ignore_single_quote = re.compile(
  408. r"(.*?)\$(\$?[\w\.]+)(?=([^']*'[^']*')*[^']*$)"
  409. )
  410. def parse(self, fmt_string: str) -> Iterator[Tuple[Any, Any, Any, Any]]:
  411. for literal_txt, field_name, format_spec, conversion in Formatter.parse(
  412. self, fmt_string
  413. ):
  414. # Find $foo patterns in the literal text.
  415. continue_from = 0
  416. txt = ""
  417. for m in self._dollar_pattern_ignore_single_quote.finditer(literal_txt):
  418. new_txt, new_field = m.group(1,2)
  419. # $$foo --> $foo
  420. if new_field.startswith("$"):
  421. txt += new_txt + new_field
  422. else:
  423. yield (txt + new_txt, new_field, "", None)
  424. txt = ""
  425. continue_from = m.end()
  426. # Re-yield the {foo} style pattern
  427. yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
  428. def __repr__(self) -> str:
  429. return "<DollarFormatter>"
  430. #-----------------------------------------------------------------------------
  431. # Utils to columnize a list of string
  432. #-----------------------------------------------------------------------------
  433. def _col_chunks(
  434. l: List[int], max_rows: int, row_first: bool = False
  435. ) -> Iterator[List[int]]:
  436. """Yield successive max_rows-sized column chunks from l."""
  437. if row_first:
  438. ncols = (len(l) // max_rows) + (len(l) % max_rows > 0)
  439. for i in range(ncols):
  440. yield [l[j] for j in range(i, len(l), ncols)]
  441. else:
  442. for i in range(0, len(l), max_rows):
  443. yield l[i:(i + max_rows)]
  444. def _find_optimal(
  445. rlist: List[int], row_first: bool, separator_size: int, displaywidth: int
  446. ) -> Dict[str, Any]:
  447. """Calculate optimal info to columnize a list of string"""
  448. for max_rows in range(1, len(rlist) + 1):
  449. col_widths = list(map(max, _col_chunks(rlist, max_rows, row_first)))
  450. sumlength = sum(col_widths)
  451. ncols = len(col_widths)
  452. if sumlength + separator_size * (ncols - 1) <= displaywidth:
  453. break
  454. return {'num_columns': ncols,
  455. 'optimal_separator_width': (displaywidth - sumlength) // (ncols - 1) if (ncols - 1) else 0,
  456. 'max_rows': max_rows,
  457. 'column_widths': col_widths
  458. }
  459. T = TypeVar("T")
  460. def _get_or_default(mylist: List[T], i: int, default: T) -> T:
  461. """return list item number, or default if don't exist"""
  462. if i >= len(mylist):
  463. return default
  464. else :
  465. return mylist[i]
  466. def get_text_list(
  467. list_: List[str], last_sep: str = " and ", sep: str = ", ", wrap_item_with: str = ""
  468. ) -> str:
  469. """
  470. Return a string with a natural enumeration of items
  471. >>> get_text_list(['a', 'b', 'c', 'd'])
  472. 'a, b, c and d'
  473. >>> get_text_list(['a', 'b', 'c'], ' or ')
  474. 'a, b or c'
  475. >>> get_text_list(['a', 'b', 'c'], ', ')
  476. 'a, b, c'
  477. >>> get_text_list(['a', 'b'], ' or ')
  478. 'a or b'
  479. >>> get_text_list(['a'])
  480. 'a'
  481. >>> get_text_list([])
  482. ''
  483. >>> get_text_list(['a', 'b'], wrap_item_with="`")
  484. '`a` and `b`'
  485. >>> get_text_list(['a', 'b', 'c', 'd'], " = ", sep=" + ")
  486. 'a + b + c = d'
  487. """
  488. if len(list_) == 0:
  489. return ''
  490. if wrap_item_with:
  491. list_ = ['%s%s%s' % (wrap_item_with, item, wrap_item_with) for
  492. item in list_]
  493. if len(list_) == 1:
  494. return list_[0]
  495. return '%s%s%s' % (
  496. sep.join(i for i in list_[:-1]),
  497. last_sep, list_[-1])