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

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

248 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 secrets 

12import textwrap 

13 

14from typing import TYPE_CHECKING, Optional 

15 

16# local 

17from ._width import width as wcwidth_width 

18from .grapheme import iter_graphemes 

19from .hyperlink import HyperlinkParams 

20from .sgr_state import propagate_sgr as _propagate_sgr 

21from .escape_sequences import ZERO_WIDTH_PATTERN, iter_sequences 

22 

23if TYPE_CHECKING: # pragma: no cover 

24 from typing import Any, Literal 

25 

26 

27class SequenceTextWrapper(textwrap.TextWrapper): 

28 """ 

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

30 

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

32 calculating text width for wrapping. 

33 

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

35 contributions from Avram Lubkin and grayjk. 

36 

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

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

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

40 

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

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

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

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

45 """ 

46 

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

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

49 tabsize: int = 8, 

50 ambiguous_width: int = 1, 

51 term_program: bool | str = False, 

52 **kwargs: Any) -> None: 

53 """ 

54 Initialize the wrapper. 

55 

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

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

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

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

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

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

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

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

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

65 

66 .. versionadded:: 0.8.0 

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

68 """ 

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

70 self.control_codes = control_codes 

71 self.tabsize = tabsize 

72 self.ambiguous_width = ambiguous_width 

73 self.term_program = term_program 

74 

75 @staticmethod 

76 def _next_hyperlink_id() -> str: 

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

78 return secrets.token_hex(4) 

79 

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

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

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

83 ambiguous_width=self.ambiguous_width, 

84 term_program=self.term_program) 

85 

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

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

88 result = [] 

89 for segment, is_seq in iter_sequences(text): 

90 if not is_seq: 

91 result.append(segment) 

92 return ''.join(result) 

93 

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

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

96 result = [] 

97 for segment, is_seq in iter_sequences(text): 

98 if is_seq: 

99 result.append(segment) 

100 return ''.join(result) 

101 

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

103 r""" 

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

105 

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

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

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

109 back. 

110 

111 OSC hyperlink sequences are treated as word boundaries:: 

112 

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

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

115 

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

117 """ 

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

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

120 # 

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

122 # attach to the following text (not preceding text). This ensures sequences 

123 # aren't lost when whitespace is dropped. 

124 # 

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

126 char_end: list[int] = [] 

127 stripped_text = '' 

128 original_pos = 0 

129 prev_was_hyperlink_close = False 

130 

131 for segment, is_seq in iter_sequences(text): 

132 if not is_seq: 

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

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

135 stripped_text += ' ' 

136 char_end.append(original_pos) 

137 for char in segment: 

138 original_pos += 1 

139 char_end.append(original_pos) 

140 stripped_text += char 

141 prev_was_hyperlink_close = False 

142 else: 

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

144 

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

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

147 # terminated on the same line. 

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

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

150 if not is_hyperlink_close: 

151 stripped_text += ' ' 

152 char_end.append(original_pos) 

153 

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

155 original_pos += len(segment) 

156 prev_was_hyperlink_close = is_hyperlink_close 

157 

158 # Add sentinel for final position 

159 char_end.append(original_pos) 

160 

161 # Use parent's _split on the stripped text 

162 # pylint: disable-next=protected-access 

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

164 

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

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

167 if not stripped_chunks and text: 

168 return [text] 

169 

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

171 result: list[str] = [] 

172 stripped_pos = 0 

173 num_chunks = len(stripped_chunks) 

174 

175 for idx, chunk in enumerate(stripped_chunks): 

176 chunk_len = len(chunk) 

177 

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

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

180 

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

182 # to include any trailing sequences. 

183 if idx == num_chunks - 1: 

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

185 else: 

186 end_orig = char_end[stripped_pos + chunk_len - 1] 

187 

188 # Extract the corresponding portion from the original text 

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

190 if start_orig != end_orig: 

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

192 stripped_pos += chunk_len 

193 

194 return result 

195 

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

197 """ 

198 Wrap chunks into lines using sequence-aware width. 

199 

200 Override TextWrapper._wrap_chunks to use _width instead of len. Follows stdlib's algorithm: 

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

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

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

204 """ 

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

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

207 # the hyperlink code in particular really pushes the complexity rating of this method. 

208 # preferring to keep it "all in one method" because of so much local state and manipulation. 

209 if not chunks: 

210 return [] 

211 

212 if self.max_lines is not None: 

213 if self.max_lines > 1: 

214 indent = self.subsequent_indent 

215 else: 

216 indent = self.initial_indent 

217 if (self._width(indent) 

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

219 > self.width): 

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

221 

222 lines: list[str] = [] 

223 is_first_line = True 

224 

225 hyperlink_state: Optional[HyperlinkParams] = None 

226 # Track the id we're using for the current hyperlink continuation 

227 current_hyperlink_id: Optional[str] = None 

228 

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

230 chunks = list(reversed(chunks)) 

231 

232 while chunks: 

233 current_line: list[str] = [] 

234 current_width = 0 

235 

236 # Get the indent and available width for current line 

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

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

239 

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

241 if hyperlink_state is not None: 

242 open_seq = HyperlinkParams( 

243 url=hyperlink_state.url, 

244 params=hyperlink_state.params, 

245 terminator=hyperlink_state.terminator, 

246 ).make_open() 

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

248 

249 # Drop leading whitespace (except at very start) 

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

251 # Only drop if there's actual whitespace text, not if it's only sequences. 

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

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

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

255 del chunks[-1] 

256 if sequences and chunks: 

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

258 

259 # Greedily add chunks that fit 

260 while chunks: 

261 chunk = chunks[-1] 

262 chunk_width = self._width(chunk) 

263 

264 if current_width + chunk_width <= line_width: 

265 current_line.append(chunks.pop()) 

266 current_width += chunk_width 

267 else: 

268 break 

269 

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

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

272 self._handle_long_word( 

273 chunks, current_line, current_width, line_width 

274 ) 

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

276 # Remove any empty chunks left by _handle_long_word 

277 while chunks and not chunks[-1]: 

278 del chunks[-1] 

279 

280 # Drop trailing whitespace 

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

282 # Only drop if there's actual whitespace text, not if it's only sequences. 

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

284 if (self.drop_whitespace and current_line and 

285 stripped_last and not stripped_last.strip()): 

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

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

288 del current_line[-1] 

289 if sequences and current_line: 

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

291 

292 if current_line: 

293 # Check whether this is a normal append or max_lines 

294 # truncation. Matches stdlib textwrap precedence: 

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

296 # remaining visible content that would need truncation. 

297 no_more_content = ( 

298 not chunks or 

299 self.drop_whitespace and 

300 len(chunks) == 1 and 

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

302 ) 

303 if (self.max_lines is None or 

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

305 no_more_content 

306 and current_width <= line_width): 

307 line_content = ''.join(current_line) 

308 

309 # Track hyperlink state through this line's content 

310 new_state = self._track_hyperlink_state(line_content, hyperlink_state) 

311 

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

313 if new_state is not None: 

314 # Ensure we have an id for continuation 

315 if current_hyperlink_id is None: 

316 if 'id=' in new_state.params: 

317 current_hyperlink_id = new_state.params 

318 elif new_state.params: 

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

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

321 current_hyperlink_id = ( 

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

323 else: 

324 current_hyperlink_id = f'id={self._next_hyperlink_id()}' 

325 line_content += HyperlinkParams( 

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

327 

328 # Also need to inject the id into the opening 

329 # sequence if it didn't have one 

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

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

332 old_open = HyperlinkParams( 

333 url=new_state.url, 

334 params=new_state.params, 

335 terminator=new_state.terminator, 

336 ).make_open() 

337 new_open = HyperlinkParams( 

338 url=new_state.url, 

339 params=current_hyperlink_id, 

340 terminator=new_state.terminator, 

341 ).make_open() 

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

343 

344 # Update state for next line, using computed id 

345 hyperlink_state = HyperlinkParams( 

346 new_state.url, current_hyperlink_id, new_state.terminator) 

347 else: 

348 hyperlink_state = None 

349 current_hyperlink_id = None # Reset id when hyperlink closes 

350 

351 # Strip trailing whitespace when drop_whitespace is enabled 

352 # (matches CPython #140627 fix behavior) 

353 if self.drop_whitespace: 

354 line_content = line_content.rstrip() 

355 lines.append(indent + line_content) 

356 is_first_line = False 

357 else: 

358 # max_lines reached with remaining content. 

359 # pop chunks until placeholder fits, then break. 

360 placeholder_w = self._width(self.placeholder) 

361 while current_line: 

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

363 if (last_text.strip() 

364 and current_width + placeholder_w <= line_width): 

365 line_content = ''.join(current_line) 

366 new_state = self._track_hyperlink_state( 

367 line_content, hyperlink_state) 

368 if new_state is not None: 

369 line_content += HyperlinkParams( 

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

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

372 break 

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

374 del current_line[-1] 

375 else: 

376 if lines: 

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

378 if (self._width(prev_line) + placeholder_w 

379 <= self.width): 

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

381 break 

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

383 break 

384 

385 return lines 

386 

387 def _track_hyperlink_state( 

388 self, text: str, 

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

390 """ 

391 Track hyperlink state through text. 

392 

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

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

395 :returns: Updated state after processing text. 

396 """ 

397 for segment, is_seq in iter_sequences(text): 

398 if is_seq: 

399 parsed_link = HyperlinkParams.parse(segment) 

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

401 state = parsed_link 

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

403 state = None 

404 return state 

405 

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

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

408 width: int) -> None: 

409 """ 

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

411 

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

413 cluster boundaries when breaking long words. 

414 """ 

415 if width < 1: 

416 space_left = 1 

417 else: 

418 space_left = width - cur_len 

419 

420 chunk = reversed_chunks[-1] 

421 

422 if self.break_long_words: 

423 break_at_hyphen = False 

424 hyphen_end = 0 

425 

426 # Handle break_on_hyphens: find last hyphen within space_left 

427 if self.break_on_hyphens: 

428 # Strip sequences to find hyphen in logical text 

429 stripped = self._strip_sequences(chunk) 

430 if len(stripped) > space_left: 

431 # Find last hyphen in the portion that fits 

432 hyphen_pos = stripped.rfind('-', 0, space_left) 

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

434 # Map back to original position including sequences 

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

436 break_at_hyphen = True 

437 

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

439 if break_at_hyphen: 

440 actual_end = hyphen_end 

441 else: 

442 actual_end = self._find_break_position(chunk, space_left) 

443 # If no progress possible (e.g., wide char exceeds line width), 

444 # force at least one grapheme to avoid infinite loop. 

445 # Only force when cur_line is empty; if line has content, 

446 # appending nothing is safe and the line will be committed. 

447 if actual_end == 0 and not cur_line: 

448 actual_end = self._find_first_grapheme_end(chunk) 

449 cur_line.append(chunk[:actual_end]) 

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

451 

452 elif not cur_line: 

453 cur_line.append(reversed_chunks.pop()) 

454 

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

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

457 stripped_idx = 0 

458 original_idx = 0 

459 

460 for segment, is_seq in iter_sequences(text): 

461 if is_seq: 

462 original_idx += len(segment) 

463 elif stripped_idx + len(segment) > stripped_pos: 

464 # Position is within this segment 

465 return original_idx + (stripped_pos - stripped_idx) 

466 else: 

467 stripped_idx += len(segment) 

468 original_idx += len(segment) 

469 

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

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

472 return original_idx # pragma: no cover 

473 

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

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

476 idx = 0 

477 width_so_far = 0 

478 

479 while idx < len(text): 

480 char = text[idx] 

481 

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

483 if char == '\x1b': 

484 match = ZERO_WIDTH_PATTERN.match(text, idx) 

485 if match: 

486 idx = match.end() 

487 continue 

488 

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

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

491 

492 grapheme_width = self._width(grapheme) 

493 if width_so_far + grapheme_width > max_width: 

494 return idx # Found break point 

495 

496 width_so_far += grapheme_width 

497 idx += len(grapheme) 

498 

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

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

501 return idx # pragma: no cover 

502 

503 def _find_first_grapheme_end(self, text: str) -> int: 

504 """Find the end position of the first grapheme.""" 

505 return len(next(iter_graphemes(text))) 

506 

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

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

509 segments = list(iter_sequences(text)) 

510 last_vis = -1 

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

512 if not is_seq and segment.rstrip(): 

513 last_vis = i 

514 if last_vis == -1: 

515 return '' 

516 result = [] 

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

518 if i < last_vis: 

519 result.append(segment) 

520 elif i == last_vis: 

521 result.append(segment.rstrip()) 

522 elif is_seq: 

523 result.append(segment) 

524 return ''.join(result) 

525 

526 

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

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

529 tabsize: int = 8, 

530 expand_tabs: bool = True, 

531 replace_whitespace: bool = True, 

532 ambiguous_width: int = 1, 

533 term_program: bool | str = False, 

534 initial_indent: str = '', 

535 subsequent_indent: str = '', 

536 fix_sentence_endings: bool = False, 

537 break_long_words: bool = True, 

538 break_on_hyphens: bool = True, 

539 drop_whitespace: bool = True, 

540 max_lines: Optional[int] = None, 

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

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

543 r""" 

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

545 

546 Like :func:`textwrap.wrap`, but measures width in display cells rather than 

547 characters, correctly handling wide characters, combining marks, and terminal 

548 escape sequences. 

549 

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

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

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

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

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

555 to spaces using ``tabsize``. 

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

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

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

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

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

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

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

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

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

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

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

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

568 

569 .. versionadded:: 0.8.0 

570 :param initial_indent: String prepended to first line. 

571 :param subsequent_indent: String prepended to subsequent lines. 

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

573 separated by exactly two spaces. 

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

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

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

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

578 Set to False to preserve whitespace. 

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

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

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

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

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

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

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

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

587 

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

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

590 begins with the active style restored:: 

591 

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

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

594 

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

596 

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

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

599 paragraph separately:: 

600 

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

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

603 ['First line. Second line.'] 

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

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

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

607 

608 .. seealso:: 

609 

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

611 Standard library text wrapping (character-based). 

612 

613 :class:`.SequenceTextWrapper` 

614 Class interface for advanced wrapping options. 

615 

616 .. versionadded:: 0.3.0 

617 

618 .. versionchanged:: 0.5.0 

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

620 

621 .. versionchanged:: 0.6.0 

622 Added ``expand_tabs``, ``replace_whitespace``, ``fix_sentence_endings``, 

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

624 

625 Example:: 

626 

627 >>> from wcwidth import wrap 

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

629 ['hello', 'world'] 

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

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

632 """ 

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

634 wrapper = SequenceTextWrapper( 

635 width=width, 

636 control_codes=control_codes, 

637 tabsize=tabsize, 

638 expand_tabs=expand_tabs, 

639 replace_whitespace=replace_whitespace, 

640 ambiguous_width=ambiguous_width, 

641 term_program=term_program, 

642 initial_indent=initial_indent, 

643 subsequent_indent=subsequent_indent, 

644 fix_sentence_endings=fix_sentence_endings, 

645 break_long_words=break_long_words, 

646 break_on_hyphens=break_on_hyphens, 

647 drop_whitespace=drop_whitespace, 

648 max_lines=max_lines, 

649 placeholder=placeholder, 

650 ) 

651 lines = wrapper.wrap(text) 

652 

653 if propagate_sgr: 

654 lines = _propagate_sgr(lines) 

655 

656 return lines