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

197 statements  

1""" 

2Utilities for working with strings and text. 

3 

4Inheritance diagram: 

5 

6.. inheritance-diagram:: IPython.utils.text 

7 :parts: 3 

8""" 

9 

10from __future__ import annotations 

11 

12import builtins 

13 

14import os 

15import re 

16import string 

17import textwrap 

18from string import Formatter 

19from pathlib import Path 

20 

21from typing import ( 

22 Any, 

23 Self, 

24) 

25from collections.abc import Sequence, Mapping, Callable, Iterator 

26 

27 

28class LSString(str): 

29 """String derivative with a special access attributes. 

30 

31 These are normal strings, but with the special attributes: 

32 

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) 

37 

38 Any values which require transformations are computed only once and 

39 cached. 

40 

41 Such strings are very useful to efficiently interact with the shell, which 

42 typically only understands whitespace-separated options for commands.""" 

43 

44 __list: list[str] 

45 __spstr: str 

46 __paths: list[Path] 

47 

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 

54 

55 l = list = property(get_list) 

56 

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 

63 

64 s = spstr = property(get_spstr) 

65 

66 def get_nlstr(self) -> Self: 

67 return self 

68 

69 n = nlstr = property(get_nlstr) 

70 

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 

77 

78 p = paths = property(get_paths) 

79 

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. 

83 

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) 

91 

92 

93class SList(list[Any]): 

94 """List derivative with a special access attributes. 

95 

96 These are normal lists, but with the special attributes: 

97 

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) 

102 

103 Any values which require transformations are computed only once and 

104 cached.""" 

105 

106 __spstr: str 

107 __nlstr: str 

108 __paths: list[Path] 

109 

110 def get_list(self) -> Self: 

111 return self 

112 

113 l = list = property(get_list) 

114 

115 def get_spstr(self) -> str: 

116 try: 

117 return self.__spstr 

118 except AttributeError: 

119 self.__spstr = ' '.join(self) 

120 return self.__spstr 

121 

122 s = spstr = property(get_spstr) 

123 

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 

130 

131 n = nlstr = property(get_nlstr) 

132 

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 

139 

140 p = paths = property(get_paths) 

141 

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) 

149 

150 This is case-insensitive. If prune is true, return all items 

151 NOT matching the pattern. 

152 

153 If field is specified, the match must occur in the specified 

154 whitespace-separated field. 

155 

156 Examples:: 

157 

158 a.grep( lambda x: x.startswith('C') ) 

159 a.grep('Cha.*log', prune=1) 

160 a.grep('chm', field=-1) 

161 """ 

162 

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 "" 

172 

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] 

181 

182 def fields(self, *fields: builtins.list[str]) -> builtins.list[builtins.list[str]]: 

183 """Collect whitespace-separated fields from string list 

184 

185 Allows quick awk-like usage of string lists. 

186 

187 Example data (in var a, created by 'a = !ls -l'):: 

188 

189 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog 

190 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython 

191 

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']`` 

196 

197 IndexErrors are ignored. 

198 

199 Without args, fields() just split()'s the strings. 

200 """ 

201 if len(fields) == 0: 

202 return [el.split() for el in self] 

203 

204 res = SList() 

205 for el in [f.split() for f in self]: 

206 lineparts = [] 

207 

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)) 

215 

216 return res 

217 

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()) 

224 

225 Example:: 

226 

227 a.sort(1, nums = True) 

228 

229 Sorts a by second field, in numerical order (so that 21 > 3) 

230 

231 """ 

232 

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 

246 

247 

248 dsu.sort() 

249 return type(self)([t[1] for t in dsu]) 

250 

251 

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. 

254 

255 indent(str, nspaces=4, ntabs=0) -> indent str by ntabs+nspaces. 

256 

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. 

269 

270 Returns 

271 ------- 

272 str : string indented by ntabs and nspaces. 

273 

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 

285 

286 

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. 

290 

291 Examples 

292 -------- 

293 :: 

294 

295 In [7]: list_strings('A single string') 

296 Out[7]: ['A single string'] 

297 

298 In [8]: list_strings(['A single string in a list']) 

299 Out[8]: ['A single string in a list'] 

300 

301 In [9]: list_strings(['A','list','of','strings']) 

302 Out[9]: ['A', 'list', 'of', 'strings'] 

303 """ 

304 

305 if isinstance(arg, str): 

306 return [arg] 

307 else: 

308 return arg 

309 

310 

311def marquee(txt: str = "", width: int = 78, mark: str = "*") -> str: 

312 """Return the input string centered in a 'marquee'. 

313 

314 Examples 

315 -------- 

316 :: 

317 

318 In [16]: marquee('A test',40) 

319 Out[16]: '**************** A test ****************' 

320 

321 In [17]: marquee('A test',40,'-') 

322 Out[17]: '---------------- A test ----------------' 

323 

324 In [18]: marquee('A test',40,' ') 

325 Out[18]: ' A test ' 

326 

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) 

334 

335 

336def format_screen(strng: str) -> str: 

337 """Format a string for screen printing. 

338 

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 

344 

345 

346def dedent(text: str) -> str: 

347 """Equivalent of textwrap.dedent that ignores unindented first line. 

348 

349 This means it will still dedent strings like: 

350 '''foo 

351 is a bar 

352 ''' 

353 

354 For use in wrap_paragraphs. 

355 """ 

356 

357 if text.startswith('\n'): 

358 # text starts with blank line, don't ignore the first line 

359 return textwrap.dedent(text) 

360 

361 # split first line 

362 splits = text.split('\n',1) 

363 if len(splits) == 1: 

364 # only one line 

365 return textwrap.dedent(text) 

366 

367 first, rest = splits 

368 # dedent everything but the first line 

369 rest = textwrap.dedent(rest) 

370 return '\n'.join([first, rest]) 

371 

372 

373def strip_email_quotes(text: str) -> str: 

374 """Strip leading email quotation characters ('>'). 

375 

376 Removes any combination of leading '>' interspersed with whitespace that 

377 appears *identically* in all lines of the input text. 

378 

379 Parameters 

380 ---------- 

381 text : str 

382 

383 Examples 

384 -------- 

385 

386 Simple uses:: 

387 

388 In [2]: strip_email_quotes('> > text') 

389 Out[2]: 'text' 

390 

391 In [3]: strip_email_quotes('> > text\\n> > more') 

392 Out[3]: 'text\\nmore' 

393 

394 Note how only the common prefix that appears in all lines is stripped:: 

395 

396 In [4]: strip_email_quotes('> > text\\n> > more\\n> more...') 

397 Out[4]: '> text\\n> more\\nmore...' 

398 

399 So if any line has no quote marks ('>'), then none are stripped from any 

400 of them :: 

401 

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 

407 

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] 

413 

414 if prefix_char in string.whitespace or prefix_char == ">": 

415 strip_len += 1 

416 else: 

417 break 

418 

419 text = "\n".join([ln[strip_len:] for ln in lines]) 

420 return text 

421 

422 

423class EvalFormatter(Formatter): 

424 """A String Formatter that allows evaluation of simple expressions. 

425 

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. 

429 

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))`. 

433 

434 This is to be used in templating cases, such as the parallel batch 

435 script templates, where simple arithmetic on arguments is useful. 

436 

437 Examples 

438 -------- 

439 :: 

440 

441 In [1]: f = EvalFormatter() 

442 In [2]: f.format('{n//4}', n=8) 

443 Out[2]: '2' 

444 

445 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello") 

446 Out[3]: 'll' 

447 """ 

448 

449 def get_field(self, name: str, args: Any, kwargs: Any) -> tuple[Any, str]: 

450 v = eval(name, kwargs, kwargs) 

451 return v, name 

452 

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. 

456 

457class FullEvalFormatter(Formatter): 

458 """A String Formatter that allows evaluation of simple expressions. 

459 

460 Any time a format key is not found in the kwargs, 

461 it will be tried as an expression in the kwargs namespace. 

462 

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. 

465 

466 Examples 

467 -------- 

468 :: 

469 

470 In [1]: f = FullEvalFormatter() 

471 In [2]: f.format('{n//4}', n=8) 

472 Out[2]: '2' 

473 

474 In [3]: f.format('{list(range(5))[2:4]}') 

475 Out[3]: '[2, 3]' 

476 

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) 

493 

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 

498 

499 if format_spec: 

500 # override format spec, to allow slicing: 

501 field_name = ':'.join([field_name, format_spec]) 

502 

503 # eval the contents of the field for the object 

504 # to be formatted 

505 obj = eval(field_name, dict(kwargs)) 

506 

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) 

510 

511 # format the object and append to the result 

512 result.append(self.format_field(obj, '')) 

513 

514 return ''.join(result) 

515 

516 

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. 

521 

522 Examples 

523 -------- 

524 :: 

525 

526 In [1]: f = DollarFormatter() 

527 In [2]: f.format('{n//4}', n=8) 

528 Out[2]: '2' 

529 

530 In [3]: f.format('23 * 76 is $result', result=23*76) 

531 Out[3]: '23 * 76 is 1748' 

532 

533 In [4]: f.format('$a or {b}', a=1, b=2) 

534 Out[4]: '1 or 2' 

535 """ 

536 

537 _dollar_pattern_ignore_single_quote = re.compile( 

538 r"(.*?)\$(\$?[\w\.]+)(?=([^']*'[^']*')*[^']*$)" 

539 ) 

540 

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() 

557 

558 # Re-yield the {foo} style pattern 

559 yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion) 

560 

561 def __repr__(self) -> str: 

562 return "<DollarFormatter>" 

563 

564 

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 

570 

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])