Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/wcwidth/textwrap.py: 11%

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

261 statements  

1""" 

2Sequence-aware text wrapping functions. 

3 

4This module provides functions for wrapping text that may contain terminal escape sequences, with 

5proper handling of Unicode grapheme clusters and character display widths. 

6""" 

7 

8from __future__ import annotations 

9 

10# std imports 

11import os 

12import secrets 

13import textwrap 

14 

15from typing import TYPE_CHECKING, Optional 

16 

17# local 

18from ._width import width as wcwidth_width 

19from .hyperlink import HyperlinkParams 

20from .sgr_state import propagate_sgr as _propagate_sgr 

21from .escape_sequences import ZERO_WIDTH_PATTERN, iter_sequences 

22 

23if os.environ.get('WCWIDTH_PYTHON', ''): 

24 # local 

25 from .grapheme import iter_graphemes 

26else: 

27 try: 

28 # local 

29 from ._wcwidth_c import iter_graphemes 

30 except ImportError: 

31 # local 

32 from .grapheme import iter_graphemes 

33 

34if TYPE_CHECKING: # pragma: no cover 

35 from typing import Any, Literal 

36 

37 

38class SequenceTextWrapper(textwrap.TextWrapper): 

39 """ 

40 Sequence-aware text wrapper extending :class:`textwrap.TextWrapper`. 

41 

42 This wrapper properly handles terminal escape sequences and Unicode grapheme clusters when 

43 calculating text width for wrapping. 

44 

45 This implementation is based on the SequenceTextWrapper from the 'blessed' library, with 

46 contributions from Avram Lubkin and grayjk. 

47 

48 The key difference from the blessed implementation is the addition of grapheme cluster support 

49 via :func:`~.iter_graphemes`, providing width calculation for ZWJ emoji sequences, VS-16 emojis 

50 and variations, regional indicator flags, and combining characters. 

51 

52 OSC 8 hyperlinks are handled specially: when a hyperlink must span multiple lines, each line 

53 receives complete open/close sequences with a shared ``id`` parameter, ensuring terminals 

54 treat the fragments as a single hyperlink for hover underlining. If the original hyperlink 

55 already has an ``id`` parameter, it is preserved; otherwise, one is generated. 

56 """ 

57 

58 def __init__(self, width: int = 70, *, 

59 control_codes: Literal['parse', 'strict', 'ignore'] = 'parse', 

60 tabsize: int = 8, 

61 ambiguous_width: int = 1, 

62 term_program: bool | str = False, 

63 **kwargs: Any) -> None: 

64 """ 

65 Initialize the wrapper. 

66 

67 :param width: Maximum line width in display cells. 

68 :param control_codes: How to handle control sequences (see :func:`~.width`). 

69 :param tabsize: Tab stop width for tab expansion. 

70 :param ambiguous_width: Width to use for East Asian Ambiguous (A) characters. 

71 :param term_program: Terminal software identifier for table correction. 

72 ``False`` (default) disables override lookup. ``True`` reads the 

73 ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection. 

74 Accepts a canonical terminal name matching :func:`list_term_programs`, 

75 such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``. 

76 

77 .. versionadded:: 0.8.0 

78 :param kwargs: Additional arguments passed to :class:`textwrap.TextWrapper`. 

79 """ 

80 super().__init__(width=width, **kwargs) 

81 self.control_codes = control_codes 

82 self.tabsize = tabsize 

83 self.ambiguous_width = ambiguous_width 

84 self.term_program = term_program 

85 

86 @staticmethod 

87 def _next_hyperlink_id() -> str: 

88 """Generate unique hyperlink id as 8-character hex string.""" 

89 return secrets.token_hex(4) 

90 

91 def _width(self, text: str) -> int: 

92 """Measure text width accounting for sequences.""" 

93 return wcwidth_width(text, control_codes=self.control_codes, tabsize=self.tabsize, 

94 ambiguous_width=self.ambiguous_width, 

95 term_program=self.term_program) 

96 

97 def _strip_sequences(self, text: str) -> str: 

98 """Strip all terminal sequences from text.""" 

99 result = [] 

100 for segment, is_seq in iter_sequences(text): 

101 if not is_seq: 

102 result.append(segment) 

103 return ''.join(result) 

104 

105 def _extract_sequences(self, text: str) -> str: 

106 """Extract only terminal sequences from text.""" 

107 result = [] 

108 for segment, is_seq in iter_sequences(text): 

109 if is_seq: 

110 result.append(segment) 

111 return ''.join(result) 

112 

113 def _split(self, text: str) -> list[str]: # pylint: disable=too-many-locals 

114 r""" 

115 Sequence-aware variant of :meth:`textwrap.TextWrapper._split`. 

116 

117 This method ensures that terminal escape sequences don't interfere with the text splitting 

118 logic, particularly for hyphen-based word breaking. It builds a position mapping from 

119 stripped text to original text, calls the parent's _split on stripped text, then maps chunks 

120 back. 

121 

122 OSC hyperlink sequences are treated as word boundaries:: 

123 

124 >>> wrap('foo \x1b]8;;https://example.com\x07link\x1b]8;;\x07 bar', 6) 

125 ['foo', '\x1b]8;;https://example.com\x07link\x1b]8;;\x07', 'bar'] 

126 

127 Both BEL (``\x07``) and ST (``\x1b\\``) terminators are supported. 

128 """ 

129 # pylint: disable=too-many-locals,too-many-branches 

130 # Build a mapping from stripped text positions to original text positions. 

131 # 

132 # Track where each character ENDS so that sequences between characters 

133 # attach to the following text, keeping them when whitespace is dropped. 

134 # 

135 # char_end[i] = position in original text right after the i-th stripped char 

136 char_end: list[int] = [] 

137 stripped_text = '' 

138 original_pos = 0 

139 prev_was_hyperlink_close = False 

140 

141 for segment, is_seq in iter_sequences(text): 

142 if not is_seq: 

143 # Conditionally insert space after hyperlink close to force word boundary 

144 if prev_was_hyperlink_close and segment and not segment[0].isspace(): 

145 stripped_text += ' ' 

146 char_end.append(original_pos) 

147 for char in segment: 

148 original_pos += 1 

149 char_end.append(original_pos) 

150 stripped_text += char 

151 prev_was_hyperlink_close = False 

152 else: 

153 is_hyperlink_close = segment.startswith(('\x1b]8;;\x1b\\', '\x1b]8;;\x07')) 

154 

155 # Conditionally insert space before OSC sequences to artificially create word 

156 # boundary, but *not* before hyperlink close sequences, to ensure hyperlink is 

157 # terminated on the same line. 

158 if (segment.startswith('\x1b]') and stripped_text and not 

159 stripped_text[-1].isspace()): 

160 if not is_hyperlink_close: 

161 stripped_text += ' ' 

162 char_end.append(original_pos) 

163 

164 # Escape sequences advance position but don't add to stripped text 

165 original_pos += len(segment) 

166 prev_was_hyperlink_close = is_hyperlink_close 

167 

168 # Add sentinel for final position 

169 char_end.append(original_pos) 

170 

171 # Use parent's _split on the stripped text 

172 # pylint: disable-next=protected-access 

173 stripped_chunks = textwrap.TextWrapper._split(self, stripped_text) 

174 

175 # Handle text that contains only sequences (no visible characters). 

176 # Return the sequences as a single chunk to preserve them. 

177 if not stripped_chunks and text: 

178 return [text] 

179 

180 # Map the chunks back to the original text with sequences 

181 result: list[str] = [] 

182 stripped_pos = 0 

183 num_chunks = len(stripped_chunks) 

184 

185 for idx, chunk in enumerate(stripped_chunks): 

186 chunk_len = len(chunk) 

187 

188 # Start is where previous character ended (or 0 for first chunk) 

189 start_orig = 0 if stripped_pos == 0 else char_end[stripped_pos - 1] 

190 

191 # End is where next character starts. For last chunk, use sentinel 

192 # to include any trailing sequences. 

193 if idx == num_chunks - 1: 

194 end_orig = char_end[-1] # sentinel includes trailing sequences 

195 else: 

196 end_orig = char_end[stripped_pos + chunk_len - 1] 

197 

198 # Extract the corresponding portion from the original text 

199 # Skip empty chunks (from virtual spaces inserted at OSC boundaries) 

200 if start_orig != end_orig: 

201 result.append(text[start_orig:end_orig]) 

202 stripped_pos += chunk_len 

203 

204 return result 

205 

206 def _wrap_chunks(self, chunks: list[str]) -> list[str]: # pylint: disable=too-many-branches 

207 """ 

208 Wrap chunks into lines using sequence-aware width. 

209 

210 Override TextWrapper._wrap_chunks to measure with _width. Follows stdlib's algorithm: 

211 greedily fill lines, handle long words. Also handle OSC hyperlink processing. When 

212 hyperlinks span multiple lines, each line gets complete open/close sequences with matching 

213 id parameters for hover underlining continuity per OSC 8 spec. 

214 """ 

215 # pylint: disable=too-many-branches,too-many-statements,too-complex,too-many-locals 

216 # pylint: disable=too-many-nested-blocks 

217 # The hyperlink code pushes the complexity rating of this method. It stays in one method 

218 # because of the shared local state. 

219 if self.width <= 0: 

220 raise ValueError('invalid width %r (must be > 0)' % self.width) 

221 if not chunks: 

222 return [] 

223 

224 if self.max_lines is not None: 

225 if self.max_lines > 1: 

226 indent = self.subsequent_indent 

227 else: 

228 indent = self.initial_indent 

229 if (self._width(indent) 

230 + self._width(self.placeholder.lstrip()) 

231 > self.width): 

232 raise ValueError("placeholder too large for max width") 

233 

234 lines: list[str] = [] 

235 is_first_line = True 

236 

237 hyperlink_state: Optional[HyperlinkParams] = None 

238 

239 # Arrange in reverse order so items can be efficiently popped 

240 chunks = list(reversed(chunks)) 

241 

242 while chunks: 

243 current_line: list[str] = [] 

244 current_width = 0 

245 

246 # Get the indent and available width for current line 

247 indent = self.initial_indent if is_first_line else self.subsequent_indent 

248 line_width = self.width - self._width(indent) 

249 

250 # If continuing a hyperlink from previous line, prepend open sequence 

251 if hyperlink_state is not None: 

252 open_seq = HyperlinkParams( 

253 url=hyperlink_state.url, 

254 params=hyperlink_state.params, 

255 terminator=hyperlink_state.terminator, 

256 ).make_open() 

257 chunks[-1] = open_seq + chunks[-1] 

258 

259 # Drop leading whitespace (except at very start) 

260 # When dropping, transfer any sequences to the next chunk. 

261 # Only drop when actual whitespace text is present. 

262 stripped = self._strip_sequences(chunks[-1]) 

263 if self.drop_whitespace and lines and stripped and not stripped.strip(): 

264 sequences = self._extract_sequences(chunks[-1]) 

265 del chunks[-1] 

266 if sequences and chunks: 

267 chunks[-1] = sequences + chunks[-1] 

268 

269 # Greedily add chunks that fit 

270 while chunks: 

271 chunk = chunks[-1] 

272 chunk_width = self._width(chunk) 

273 

274 if current_width + chunk_width <= line_width: 

275 current_line.append(chunks.pop()) 

276 current_width += chunk_width 

277 else: 

278 break 

279 

280 # Handle chunk that's too long for any line 

281 if chunks and self._width(chunks[-1]) > line_width: 

282 self._handle_long_word( 

283 chunks, current_line, current_width, line_width 

284 ) 

285 current_width = self._width(''.join(current_line)) 

286 # Remove any empty chunks left by _handle_long_word 

287 while chunks and not chunks[-1]: 

288 del chunks[-1] 

289 

290 # Drop trailing whitespace 

291 # When dropping, transfer any sequences to the previous chunk. 

292 # Only drop when actual whitespace text is present. 

293 stripped_last = self._strip_sequences(current_line[-1]) if current_line else '' 

294 if (self.drop_whitespace and current_line and 

295 stripped_last and not stripped_last.strip()): 

296 sequences = self._extract_sequences(current_line[-1]) 

297 current_width -= self._width(current_line[-1]) 

298 del current_line[-1] 

299 if sequences and current_line: 

300 current_line[-1] = current_line[-1] + sequences 

301 

302 if current_line: 

303 # Check whether this is a normal append or max_lines 

304 # truncation. Matches stdlib textwrap precedence: 

305 # normal if max_lines not set, not yet reached, or no 

306 # remaining visible content that would need truncation. 

307 no_more_content = ( 

308 not chunks or 

309 self.drop_whitespace and 

310 len(chunks) == 1 and 

311 not self._strip_sequences(chunks[0]).strip() 

312 ) 

313 if (self.max_lines is None or 

314 len(lines) + 1 < self.max_lines or 

315 no_more_content 

316 and current_width <= line_width): 

317 line_content = ''.join(current_line) 

318 

319 # Track hyperlink state through this line's content 

320 new_state = self._track_hyperlink_state(line_content, hyperlink_state) 

321 

322 # If we end inside a hyperlink, append close sequence 

323 if new_state is not None: 

324 # Use the active link's params; wrapper state can leak a prior link's id. 

325 if 'id=' in new_state.params: 

326 continuation_params = new_state.params 

327 elif new_state.params: 

328 # Prepend id to existing params. Per OSC 8 spec, params can have 

329 # multiple key=value pairs separated by ':'. 

330 continuation_params = ( 

331 f'id={self._next_hyperlink_id()}:{new_state.params}') 

332 else: 

333 continuation_params = f'id={self._next_hyperlink_id()}' 

334 line_content += HyperlinkParams( 

335 terminator=new_state.terminator, url='').make_close() 

336 

337 # Also need to inject the id into the opening 

338 # sequence if it didn't have one 

339 if 'id=' not in new_state.params: 

340 # Find and replace the original open sequence with one that has id 

341 old_open = HyperlinkParams( 

342 url=new_state.url, 

343 params=new_state.params, 

344 terminator=new_state.terminator, 

345 ).make_open() 

346 new_open = HyperlinkParams( 

347 url=new_state.url, 

348 params=continuation_params, 

349 terminator=new_state.terminator, 

350 ).make_open() 

351 line_content = line_content.replace(old_open, new_open, 1) 

352 

353 # Update state for next line, using computed params 

354 hyperlink_state = HyperlinkParams( 

355 new_state.url, continuation_params, new_state.terminator) 

356 else: 

357 hyperlink_state = None 

358 

359 # Strip trailing whitespace when drop_whitespace is enabled 

360 # (matches CPython #140627 fix behavior) 

361 if self.drop_whitespace: 

362 line_content = line_content.rstrip() 

363 if not line_content: 

364 continue 

365 lines.append(indent + line_content) 

366 is_first_line = False 

367 else: 

368 # max_lines reached with remaining content. 

369 # pop chunks until placeholder fits, then break. 

370 placeholder_w = self._width(self.placeholder) 

371 while current_line: 

372 last_text = self._strip_sequences(current_line[-1]) 

373 if (last_text.strip() 

374 and current_width + placeholder_w <= line_width): 

375 line_content = ''.join(current_line) 

376 new_state = self._track_hyperlink_state( 

377 line_content, hyperlink_state) 

378 if new_state is not None: 

379 line_content += HyperlinkParams( 

380 terminator=new_state.terminator, url='').make_close() 

381 lines.append(indent + line_content + self.placeholder) 

382 break 

383 current_width -= self._width(current_line[-1]) 

384 del current_line[-1] 

385 else: 

386 if lines: 

387 prev_line = self._rstrip_visible(lines[-1]) 

388 if (self._width(prev_line) + placeholder_w 

389 <= self.width): 

390 lines[-1] = prev_line + self.placeholder 

391 break 

392 lines.append(indent + self.placeholder.lstrip()) 

393 break 

394 

395 return lines 

396 

397 def _track_hyperlink_state( 

398 self, text: str, 

399 state: Optional[HyperlinkParams]) -> Optional[HyperlinkParams]: 

400 """ 

401 Track hyperlink state through text. 

402 

403 :param text: Text to scan for hyperlink sequences. 

404 :param state: Current state or None if outside hyperlink. 

405 :returns: Updated state after processing text. 

406 """ 

407 for segment, is_seq in iter_sequences(text): 

408 if is_seq: 

409 parsed_link = HyperlinkParams.parse(segment) 

410 if parsed_link is not None and parsed_link.url: # has URL = open 

411 state = parsed_link 

412 elif segment.startswith(('\x1b]8;;\x1b\\', '\x1b]8;;\x07')): # close 

413 state = None 

414 return state 

415 

416 def _handle_long_word(self, reversed_chunks: list[str], 

417 cur_line: list[str], cur_len: int, 

418 width: int) -> None: 

419 """ 

420 Sequence-aware :meth:`textwrap.TextWrapper._handle_long_word`. 

421 

422 This method ensures that word boundaries are not broken mid-sequence, and respects grapheme 

423 cluster boundaries when breaking long words. 

424 """ 

425 if width < 1: 

426 space_left = 1 

427 else: 

428 space_left = width - cur_len 

429 

430 chunk = reversed_chunks[-1] 

431 

432 if self.break_long_words: 

433 break_at_hyphen = False 

434 hyphen_end = 0 

435 # End of the prefix that fits within space_left by display width. 

436 prefix_end = self._find_break_position(chunk, space_left) 

437 

438 # Handle break_on_hyphens: find last hyphen in the portion that fits. 

439 if self.break_on_hyphens: 

440 stripped = self._strip_sequences(chunk[:prefix_end]) 

441 hyphen_pos = stripped.rfind('-') 

442 if hyphen_pos > 0 and any(c != '-' for c in stripped[:hyphen_pos]): 

443 # Map back to original position including sequences 

444 hyphen_end = self._map_stripped_pos_to_original(chunk, hyphen_pos + 1) 

445 break_at_hyphen = True 

446 

447 # Break at grapheme boundaries to avoid splitting multi-codepoint characters 

448 if break_at_hyphen: 

449 actual_end = hyphen_end 

450 else: 

451 actual_end = prefix_end 

452 # Include first visible unit when break would take only leading sequences. 

453 if not cur_line and ( 

454 actual_end == 0 

455 or (actual_end < len(chunk) 

456 and self._width(chunk[:actual_end]) == 0)): 

457 actual_end = self._find_first_visible_break(chunk) 

458 cur_line.append(chunk[:actual_end]) 

459 reversed_chunks[-1] = chunk[actual_end:] 

460 

461 elif not cur_line: 

462 cur_line.append(reversed_chunks.pop()) 

463 

464 def _map_stripped_pos_to_original(self, text: str, stripped_pos: int) -> int: 

465 """Map a position in stripped text back to original text position.""" 

466 stripped_idx = 0 

467 original_idx = 0 

468 

469 for segment, is_seq in iter_sequences(text): 

470 if is_seq: 

471 original_idx += len(segment) 

472 elif stripped_idx + len(segment) > stripped_pos: 

473 # Position is within this segment 

474 return original_idx + (stripped_pos - stripped_idx) 

475 else: 

476 stripped_idx += len(segment) 

477 original_idx += len(segment) 

478 

479 # Caller guarantees stripped_pos < total stripped chars, so we always 

480 # return from within the loop. This line satisfies the type checker. 

481 return original_idx # pragma: no cover 

482 

483 def _find_break_position(self, text: str, max_width: int) -> int: 

484 """Find string index in text that fits within max_width cells.""" 

485 idx = 0 

486 width_so_far = 0 

487 

488 while idx < len(text): 

489 char = text[idx] 

490 

491 # Skip escape sequences (they don't add width) 

492 if char == '\x1b': 

493 match = ZERO_WIDTH_PATTERN.match(text, idx) 

494 if match: 

495 idx = match.end() 

496 continue 

497 

498 # Get grapheme (use start= to avoid slice allocation) 

499 grapheme = next(iter_graphemes(text, start=idx)) 

500 

501 grapheme_width = self._width(grapheme) 

502 if width_so_far + grapheme_width > max_width: 

503 return idx # Found break point 

504 

505 width_so_far += grapheme_width 

506 idx += len(grapheme) 

507 

508 # Caller guarantees chunk_width > max_width, so a grapheme always 

509 # exceeds and we return from within the loop. Type checker requires this. 

510 return idx # pragma: no cover 

511 

512 def _find_first_visible_break(self, text: str) -> int: 

513 """End of leading escape sequences plus the first grapheme.""" 

514 idx = 0 

515 while idx < len(text) and text[idx] == '\x1b': 

516 match = ZERO_WIDTH_PATTERN.match(text, idx) 

517 if match is None: 

518 break 

519 idx = match.end() 

520 return idx + len(next(iter_graphemes(text, start=idx))) 

521 

522 def _rstrip_visible(self, text: str) -> str: 

523 """Strip trailing visible whitespace, preserving trailing sequences.""" 

524 segments = list(iter_sequences(text)) 

525 last_vis = -1 

526 for i, (segment, is_seq) in enumerate(segments): 

527 if not is_seq and segment.rstrip(): 

528 last_vis = i 

529 if last_vis == -1: 

530 return '' 

531 result = [] 

532 for i, (segment, is_seq) in enumerate(segments): 

533 if i < last_vis: 

534 result.append(segment) 

535 elif i == last_vis: 

536 result.append(segment.rstrip()) 

537 elif is_seq: 

538 result.append(segment) 

539 return ''.join(result) 

540 

541 

542def wrap(text: str, width: int = 70, *, 

543 control_codes: Literal['parse', 'strict', 'ignore'] = 'parse', 

544 tabsize: int = 8, 

545 expand_tabs: bool = True, 

546 replace_whitespace: bool = True, 

547 ambiguous_width: int = 1, 

548 term_program: bool | str = False, 

549 initial_indent: str = '', 

550 subsequent_indent: str = '', 

551 fix_sentence_endings: bool = False, 

552 break_long_words: bool = True, 

553 break_on_hyphens: bool = True, 

554 drop_whitespace: bool = True, 

555 max_lines: Optional[int] = None, 

556 placeholder: str = ' [...]', 

557 propagate_sgr: bool = True) -> list[str]: 

558 r""" 

559 Wrap text to fit within given width, returning a list of wrapped lines. 

560 

561 Like :func:`textwrap.wrap`, but measures width in display cells, correctly 

562 handling wide characters, combining marks, and terminal 

563 escape sequences. 

564 

565 :param text: Text to wrap, may contain terminal sequences. 

566 :param width: Maximum line width in display cells. 

567 :param control_codes: How to handle terminal sequences (see :func:`~.width`). 

568 :param tabsize: Tab stop width for tab expansion. 

569 :param expand_tabs: If True (default), tab characters are expanded 

570 to spaces using ``tabsize``. 

571 :param replace_whitespace: If True (default), each whitespace character 

572 is replaced with a single space after tab expansion. When False, 

573 control whitespace like ``\n`` has zero display width (unlike 

574 :func:`textwrap.wrap` which counts ``len()``), so wrap points 

575 may differ from stdlib for non-space whitespace characters. 

576 :param ambiguous_width: Width to use for East Asian Ambiguous (A) 

577 characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. 

578 :param term_program: Terminal software identifier for table correction. 

579 ``False`` (default) disables override lookup. ``True`` reads the 

580 ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection. 

581 Accepts a canonical terminal name matching :func:`list_term_programs`, 

582 such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``. 

583 

584 .. versionadded:: 0.8.0 

585 :param initial_indent: String prepended to first line. 

586 :param subsequent_indent: String prepended to subsequent lines. 

587 :param fix_sentence_endings: If True, ensure sentences are always 

588 separated by exactly two spaces. 

589 :param break_long_words: If True, break words longer than width. 

590 :param break_on_hyphens: If True, allow breaking at hyphens. 

591 :param drop_whitespace: If True (default), whitespace at the beginning 

592 and end of each line (after wrapping but before indenting) is dropped. 

593 Set to False to preserve whitespace. 

594 :param max_lines: If set, output contains at most this many lines, with 

595 ``placeholder`` appended to the last line if the text was truncated. 

596 :param placeholder: String appended to the last line when text is 

597 truncated by ``max_lines``. Default is ``' [...]'``. 

598 :param propagate_sgr: If True (default), SGR (terminal styling) sequences 

599 are propagated across wrapped lines. Each line ends with a reset 

600 sequence and the next line begins with the active style restored. 

601 :returns: List of wrapped lines without trailing newlines. 

602 

603 SGR (terminal styling) sequences are propagated across wrapped lines 

604 by default. Each line ends with a reset sequence and the next line 

605 begins with the active style restored:: 

606 

607 >>> wrap('\x1b[1;34mHello world\x1b[0m', width=6) 

608 ['\x1b[1;34mHello\x1b[0m', '\x1b[1;34mworld\x1b[0m'] 

609 

610 Set ``propagate_sgr=False`` to disable this behavior. 

611 

612 Like :func:`textwrap.wrap`, newlines in the input text are treated as 

613 whitespace and collapsed. To preserve paragraph breaks, wrap each 

614 paragraph separately:: 

615 

616 >>> text = 'First line.\nSecond line.' 

617 >>> wrap(text, 40) # newline collapsed to space 

618 ['First line. Second line.'] 

619 >>> [line for para in text.split('\n') 

620 ... for line in (wrap(para, 40) if para else [''])] 

621 ['First line.', 'Second line.'] 

622 

623 .. note:: 

624 

625 Segmentation follows UAX #29 grapheme clusters. Tamil, Kannada and Sinhala may break in the 

626 middle of a conjunct and fall short of ``width``. Correct rendering of these languages is 

627 not specified by unicode.org standards, and no terminal emulator or editor handles them 

628 legibly, so this library function makes no attempt at accommodating them. 

629 

630 .. seealso:: 

631 

632 :func:`textwrap.wrap`, :class:`textwrap.TextWrapper` 

633 Standard library text wrapping (character-based). 

634 

635 :class:`.SequenceTextWrapper` 

636 Class interface for advanced wrapping options. 

637 

638 .. versionadded:: 0.3.0 

639 

640 .. versionchanged:: 0.5.0 

641 Added ``propagate_sgr`` parameter (default True). 

642 

643 .. versionchanged:: 0.6.0 

644 Added ``expand_tabs``, ``replace_whitespace``, ``fix_sentence_endings``, 

645 ``drop_whitespace``, ``max_lines``, and ``placeholder`` parameters. 

646 

647 Example:: 

648 

649 >>> from wcwidth import wrap 

650 >>> wrap('hello world', 5) 

651 ['hello', 'world'] 

652 >>> wrap('中文字符', 4) # CJK characters (2 cells each) 

653 ['中文', '字符'] 

654 """ 

655 # pylint: disable=too-many-arguments,too-many-locals 

656 wrapper = SequenceTextWrapper( 

657 width=width, 

658 control_codes=control_codes, 

659 tabsize=tabsize, 

660 expand_tabs=expand_tabs, 

661 replace_whitespace=replace_whitespace, 

662 ambiguous_width=ambiguous_width, 

663 term_program=term_program, 

664 initial_indent=initial_indent, 

665 subsequent_indent=subsequent_indent, 

666 fix_sentence_endings=fix_sentence_endings, 

667 break_long_words=break_long_words, 

668 break_on_hyphens=break_on_hyphens, 

669 drop_whitespace=drop_whitespace, 

670 max_lines=max_lines, 

671 placeholder=placeholder, 

672 ) 

673 lines = wrapper.wrap(text) 

674 

675 if propagate_sgr: 

676 lines = _propagate_sgr(lines) 

677 

678 return lines