Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/IPython/utils/text.py: 31%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Utilities for working with strings and text.
4Inheritance diagram:
6.. inheritance-diagram:: IPython.utils.text
7 :parts: 3
8"""
10from __future__ import annotations
12import builtins
14import os
15import re
16import string
17import textwrap
18from string import Formatter
19from pathlib import Path
21from typing import (
22 Any,
23 Self,
24)
25from collections.abc import Sequence, Mapping, Callable, Iterator
28class LSString(str):
29 """String derivative with a special access attributes.
31 These are normal strings, but with the special attributes:
33 .l (or .list) : value as list (split on newlines).
34 .n (or .nlstr): original value (the string itself).
35 .s (or .spstr): value as whitespace-separated string.
36 .p (or .paths): list of pathlib.Path objects (existing files only)
38 Any values which require transformations are computed only once and
39 cached.
41 Such strings are very useful to efficiently interact with the shell, which
42 typically only understands whitespace-separated options for commands."""
44 __list: list[str]
45 __spstr: str
46 __paths: list[Path]
48 def get_list(self) -> list[str]:
49 try:
50 return self.__list
51 except AttributeError:
52 self.__list = self.split('\n')
53 return self.__list
55 l = list = property(get_list)
57 def get_spstr(self) -> str:
58 try:
59 return self.__spstr
60 except AttributeError:
61 self.__spstr = self.replace('\n',' ')
62 return self.__spstr
64 s = spstr = property(get_spstr)
66 def get_nlstr(self) -> Self:
67 return self
69 n = nlstr = property(get_nlstr)
71 def get_paths(self) -> builtins.list[Path]:
72 try:
73 return self.__paths
74 except AttributeError:
75 self.__paths = [Path(p) for p in self.split('\n') if os.path.exists(p)]
76 return self.__paths
78 p = paths = property(get_paths)
80# FIXME: We need to reimplement type specific displayhook and then add this
81# back as a custom printer. This should also be moved outside utils into the
82# core.
84# def print_lsstring(arg):
85# """ Prettier (non-repr-like) and more informative printer for LSString """
86# print("LSString (.p, .n, .l, .s available). Value:")
87# print(arg)
88#
89#
90# print_lsstring = result_display.register(LSString)(print_lsstring)
93class SList(list[Any]):
94 """List derivative with a special access attributes.
96 These are normal lists, but with the special attributes:
98 * .l (or .list) : value as list (the list itself).
99 * .n (or .nlstr): value as a string, joined on newlines.
100 * .s (or .spstr): value as a string, joined on spaces.
101 * .p (or .paths): list of pathlib.Path objects (existing files only)
103 Any values which require transformations are computed only once and
104 cached."""
106 __spstr: str
107 __nlstr: str
108 __paths: list[Path]
110 def get_list(self) -> Self:
111 return self
113 l = list = property(get_list)
115 def get_spstr(self) -> str:
116 try:
117 return self.__spstr
118 except AttributeError:
119 self.__spstr = ' '.join(self)
120 return self.__spstr
122 s = spstr = property(get_spstr)
124 def get_nlstr(self) -> str:
125 try:
126 return self.__nlstr
127 except AttributeError:
128 self.__nlstr = '\n'.join(self)
129 return self.__nlstr
131 n = nlstr = property(get_nlstr)
133 def get_paths(self) -> builtins.list[Path]:
134 try:
135 return self.__paths
136 except AttributeError:
137 self.__paths = [Path(p) for p in self if os.path.exists(p)]
138 return self.__paths
140 p = paths = property(get_paths)
142 def grep(
143 self,
144 pattern: str | Callable[[Any], re.Match[str] | None],
145 prune: bool = False,
146 field: int | None = None,
147 ) -> Self:
148 """Return all strings matching 'pattern' (a regex or callable)
150 This is case-insensitive. If prune is true, return all items
151 NOT matching the pattern.
153 If field is specified, the match must occur in the specified
154 whitespace-separated field.
156 Examples::
158 a.grep( lambda x: x.startswith('C') )
159 a.grep('Cha.*log', prune=1)
160 a.grep('chm', field=-1)
161 """
163 def match_target(s: str) -> str:
164 if field is None:
165 return s
166 parts = s.split()
167 try:
168 tgt = parts[field]
169 return tgt
170 except IndexError:
171 return ""
173 if isinstance(pattern, str):
174 pred = lambda x : re.search(pattern, x, re.IGNORECASE)
175 else:
176 pred = pattern
177 if not prune:
178 return type(self)([el for el in self if pred(match_target(el))]) # type: ignore [no-untyped-call]
179 else:
180 return type(self)([el for el in self if not pred(match_target(el))]) # type: ignore [no-untyped-call]
182 def fields(self, *fields: builtins.list[str]) -> builtins.list[builtins.list[str]]:
183 """Collect whitespace-separated fields from string list
185 Allows quick awk-like usage of string lists.
187 Example data (in var a, created by 'a = !ls -l')::
189 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
190 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
192 * ``a.fields(0)`` is ``['-rwxrwxrwx', 'drwxrwxrwx+']``
193 * ``a.fields(1,0)`` is ``['1 -rwxrwxrwx', '6 drwxrwxrwx+']``
194 (note the joining by space).
195 * ``a.fields(-1)`` is ``['ChangeLog', 'IPython']``
197 IndexErrors are ignored.
199 Without args, fields() just split()'s the strings.
200 """
201 if len(fields) == 0:
202 return [el.split() for el in self]
204 res = SList()
205 for el in [f.split() for f in self]:
206 lineparts = []
208 for fd in fields:
209 try:
210 lineparts.append(el[fd])
211 except IndexError:
212 pass
213 if lineparts:
214 res.append(" ".join(lineparts))
216 return res
218 def sort( # type:ignore[override]
219 self,
220 field: builtins.list[str] | None = None,
221 nums: bool = False,
222 ) -> Self:
223 """sort by specified fields (see fields())
225 Example::
227 a.sort(1, nums = True)
229 Sorts a by second field, in numerical order (so that 21 > 3)
231 """
233 #decorate, sort, undecorate
234 if field is not None:
235 dsu = [[SList([line]).fields(field), line] for line in self]
236 else:
237 dsu = [[line, line] for line in self]
238 if nums:
239 for i in range(len(dsu)):
240 numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
241 try:
242 n = int(numstr)
243 except ValueError:
244 n = 0
245 dsu[i][0] = n
248 dsu.sort()
249 return type(self)([t[1] for t in dsu])
252def indent(instr: str, nspaces: int = 4, ntabs: int = 0, flatten: bool = False) -> str:
253 """Indent a string a given number of spaces or tabstops.
255 indent(str, nspaces=4, ntabs=0) -> indent str by ntabs+nspaces.
257 Parameters
258 ----------
259 instr : str
260 The string to be indented.
261 nspaces : int (default: 4)
262 The number of spaces to be indented.
263 ntabs : int (default: 0)
264 The number of tabs to be indented.
265 flatten : bool (default: False)
266 Whether to scrub existing indentation. If True, all lines will be
267 aligned to the same indentation. If False, existing indentation will
268 be strictly increased.
270 Returns
271 -------
272 str : string indented by ntabs and nspaces.
274 """
275 ind = "\t" * ntabs + " " * nspaces
276 if flatten:
277 pat = re.compile(r'^\s*', re.MULTILINE)
278 else:
279 pat = re.compile(r'^', re.MULTILINE)
280 outstr = re.sub(pat, ind, instr)
281 if outstr.endswith(os.linesep+ind):
282 return outstr[:-len(ind)]
283 else:
284 return outstr
287def list_strings(arg: str | list[str]) -> list[str]:
288 """Always return a list of strings, given a string or list of strings
289 as input.
291 Examples
292 --------
293 ::
295 In [7]: list_strings('A single string')
296 Out[7]: ['A single string']
298 In [8]: list_strings(['A single string in a list'])
299 Out[8]: ['A single string in a list']
301 In [9]: list_strings(['A','list','of','strings'])
302 Out[9]: ['A', 'list', 'of', 'strings']
303 """
305 if isinstance(arg, str):
306 return [arg]
307 else:
308 return arg
311def marquee(txt: str = "", width: int = 78, mark: str = "*") -> str:
312 """Return the input string centered in a 'marquee'.
314 Examples
315 --------
316 ::
318 In [16]: marquee('A test',40)
319 Out[16]: '**************** A test ****************'
321 In [17]: marquee('A test',40,'-')
322 Out[17]: '---------------- A test ----------------'
324 In [18]: marquee('A test',40,' ')
325 Out[18]: ' A test '
327 """
328 if not txt:
329 return (mark*width)[:width]
330 nmark = (width-len(txt)-2)//len(mark)//2
331 if nmark < 0: nmark =0
332 marks = mark*nmark
333 return '%s %s %s' % (marks,txt,marks)
336def format_screen(strng: str) -> str:
337 """Format a string for screen printing.
339 This removes some latex-type format codes."""
340 # Paragraph continue
341 par_re = re.compile(r'\\$',re.MULTILINE)
342 strng = par_re.sub('',strng)
343 return strng
346def dedent(text: str) -> str:
347 """Equivalent of textwrap.dedent that ignores unindented first line.
349 This means it will still dedent strings like:
350 '''foo
351 is a bar
352 '''
354 For use in wrap_paragraphs.
355 """
357 if text.startswith('\n'):
358 # text starts with blank line, don't ignore the first line
359 return textwrap.dedent(text)
361 # split first line
362 splits = text.split('\n',1)
363 if len(splits) == 1:
364 # only one line
365 return textwrap.dedent(text)
367 first, rest = splits
368 # dedent everything but the first line
369 rest = textwrap.dedent(rest)
370 return '\n'.join([first, rest])
373def strip_email_quotes(text: str) -> str:
374 """Strip leading email quotation characters ('>').
376 Removes any combination of leading '>' interspersed with whitespace that
377 appears *identically* in all lines of the input text.
379 Parameters
380 ----------
381 text : str
383 Examples
384 --------
386 Simple uses::
388 In [2]: strip_email_quotes('> > text')
389 Out[2]: 'text'
391 In [3]: strip_email_quotes('> > text\\n> > more')
392 Out[3]: 'text\\nmore'
394 Note how only the common prefix that appears in all lines is stripped::
396 In [4]: strip_email_quotes('> > text\\n> > more\\n> more...')
397 Out[4]: '> text\\n> more\\nmore...'
399 So if any line has no quote marks ('>'), then none are stripped from any
400 of them ::
402 In [5]: strip_email_quotes('> > text\\n> > more\\nlast different')
403 Out[5]: '> > text\\n> > more\\nlast different'
404 """
405 lines = text.splitlines()
406 strip_len = 0
408 for characters in zip(*lines):
409 # Check if all characters in this position are the same
410 if len(set(characters)) > 1:
411 break
412 prefix_char = characters[0]
414 if prefix_char in string.whitespace or prefix_char == ">":
415 strip_len += 1
416 else:
417 break
419 text = "\n".join([ln[strip_len:] for ln in lines])
420 return text
423class EvalFormatter(Formatter):
424 """A String Formatter that allows evaluation of simple expressions.
426 Note that this version interprets a `:` as specifying a format string (as per
427 standard string formatting), so if slicing is required, you must explicitly
428 create a slice.
430 Note that on Python 3.14+ this version interprets `[]` as indexing operator
431 so you need to use generators instead of list comprehensions, for example:
432 `list(i for i in range(10))`.
434 This is to be used in templating cases, such as the parallel batch
435 script templates, where simple arithmetic on arguments is useful.
437 Examples
438 --------
439 ::
441 In [1]: f = EvalFormatter()
442 In [2]: f.format('{n//4}', n=8)
443 Out[2]: '2'
445 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
446 Out[3]: 'll'
447 """
449 def get_field(self, name: str, args: Any, kwargs: Any) -> tuple[Any, str]:
450 v = eval(name, kwargs, kwargs)
451 return v, name
453#XXX: As of Python 3.4, the format string parsing no longer splits on a colon
454# inside [], so EvalFormatter can handle slicing. Once we only support 3.4 and
455# above, it should be possible to remove FullEvalFormatter.
457class FullEvalFormatter(Formatter):
458 """A String Formatter that allows evaluation of simple expressions.
460 Any time a format key is not found in the kwargs,
461 it will be tried as an expression in the kwargs namespace.
463 Note that this version allows slicing using [1:2], so you cannot specify
464 a format string. Use :class:`EvalFormatter` to permit format strings.
466 Examples
467 --------
468 ::
470 In [1]: f = FullEvalFormatter()
471 In [2]: f.format('{n//4}', n=8)
472 Out[2]: '2'
474 In [3]: f.format('{list(range(5))[2:4]}')
475 Out[3]: '[2, 3]'
477 In [4]: f.format('{3*2}')
478 Out[4]: '6'
479 """
480 # copied from Formatter._vformat with minor changes to allow eval
481 # and replace the format_spec code with slicing
482 def vformat(
483 self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any]
484 ) -> str:
485 result = []
486 conversion: str | None
487 for literal_text, field_name, format_spec, conversion in self.parse(
488 format_string
489 ):
490 # output the literal text
491 if literal_text:
492 result.append(literal_text)
494 # if there's a field, output it
495 if field_name is not None:
496 # this is some markup, find the object and do
497 # the formatting
499 if format_spec:
500 # override format spec, to allow slicing:
501 field_name = ':'.join([field_name, format_spec])
503 # eval the contents of the field for the object
504 # to be formatted
505 obj = eval(field_name, dict(kwargs))
507 # do any conversion on the resulting object
508 # type issue in typeshed, fined in https://github.com/python/typeshed/pull/11377
509 obj = self.convert_field(obj, conversion)
511 # format the object and append to the result
512 result.append(self.format_field(obj, ''))
514 return ''.join(result)
517class DollarFormatter(FullEvalFormatter):
518 """Formatter allowing Itpl style $foo replacement, for names and attribute
519 access only. Standard {foo} replacement also works, and allows full
520 evaluation of its arguments.
522 Examples
523 --------
524 ::
526 In [1]: f = DollarFormatter()
527 In [2]: f.format('{n//4}', n=8)
528 Out[2]: '2'
530 In [3]: f.format('23 * 76 is $result', result=23*76)
531 Out[3]: '23 * 76 is 1748'
533 In [4]: f.format('$a or {b}', a=1, b=2)
534 Out[4]: '1 or 2'
535 """
537 _dollar_pattern_ignore_single_quote = re.compile(
538 r"(.*?)\$(\$?[\w\.]+)(?=([^']*'[^']*')*[^']*$)"
539 )
541 def parse(self, fmt_string: str) -> Iterator[tuple[Any, Any, Any, Any]]:
542 for literal_txt, field_name, format_spec, conversion in Formatter.parse(
543 self, fmt_string
544 ):
545 # Find $foo patterns in the literal text.
546 continue_from = 0
547 txt = ""
548 for m in self._dollar_pattern_ignore_single_quote.finditer(literal_txt):
549 new_txt, new_field = m.group(1,2)
550 # $$foo --> $foo
551 if new_field.startswith("$"):
552 txt += new_txt + new_field
553 else:
554 yield (txt + new_txt, new_field, "", None)
555 txt = ""
556 continue_from = m.end()
558 # Re-yield the {foo} style pattern
559 yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
561 def __repr__(self) -> str:
562 return "<DollarFormatter>"
565def get_text_list(
566 list_: list[str], last_sep: str = " and ", sep: str = ", ", wrap_item_with: str = ""
567) -> str:
568 """
569 Return a string with a natural enumeration of items
571 >>> get_text_list(['a', 'b', 'c', 'd'])
572 'a, b, c and d'
573 >>> get_text_list(['a', 'b', 'c'], ' or ')
574 'a, b or c'
575 >>> get_text_list(['a', 'b', 'c'], ', ')
576 'a, b, c'
577 >>> get_text_list(['a', 'b'], ' or ')
578 'a or b'
579 >>> get_text_list(['a'])
580 'a'
581 >>> get_text_list([])
582 ''
583 >>> get_text_list(['a', 'b'], wrap_item_with="`")
584 '`a` and `b`'
585 >>> get_text_list(['a', 'b', 'c', 'd'], " = ", sep=" + ")
586 'a + b + c = d'
587 """
588 if len(list_) == 0:
589 return ''
590 if wrap_item_with:
591 list_ = ['%s%s%s' % (wrap_item_with, item, wrap_item_with) for
592 item in list_]
593 if len(list_) == 1:
594 return list_[0]
595 return '%s%s%s' % (
596 sep.join(i for i in list_[:-1]),
597 last_sep, list_[-1])