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

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

7from __future__ import annotations 

8 

9# std imports 

10import re 

11import secrets 

12import textwrap 

13 

14from typing import TYPE_CHECKING, NamedTuple 

15 

16# local 

17from .wcwidth import width as _width 

18from .wcwidth import iter_sequences 

19from .grapheme import iter_graphemes 

20from .sgr_state import propagate_sgr as _propagate_sgr 

21from .escape_sequences import ZERO_WIDTH_PATTERN 

22 

23if TYPE_CHECKING: # pragma: no cover 

24 from typing import Any, Literal 

25 

26 

27class _HyperlinkState(NamedTuple): 

28 """State for tracking an open OSC 8 hyperlink across line breaks.""" 

29 

30 url: str # hyperlink target URL 

31 params: str # id=xxx and other key=value pairs separated by : 

32 terminator: str # BEL (\x07) or ST (\x1b\\) 

33 

34 

35# Hyperlink parsing: captures (params, url, terminator) 

36_HYPERLINK_OPEN_RE = re.compile(r'\x1b]8;([^;]*);([^\x07\x1b]*)(\x07|\x1b\\)') 

37 

38 

39def _parse_hyperlink_open(seq: str) -> _HyperlinkState | None: 

40 """Parse OSC 8 open sequence, return state or None.""" 

41 if (m := _HYPERLINK_OPEN_RE.match(seq)): 

42 return _HyperlinkState(url=m.group(2), params=m.group(1), terminator=m.group(3)) 

43 return None 

44 

45 

46def _make_hyperlink_open(url: str, params: str, terminator: str) -> str: 

47 """Generate OSC 8 open sequence.""" 

48 return f'\x1b]8;{params};{url}{terminator}' 

49 

50 

51def _make_hyperlink_close(terminator: str) -> str: 

52 """Generate OSC 8 close sequence.""" 

53 return f'\x1b]8;;{terminator}' 

54 

55 

56class SequenceTextWrapper(textwrap.TextWrapper): 

57 """ 

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

59 

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

61 calculating text width for wrapping. 

62 

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

64 contributions from Avram Lubkin and grayjk. 

65 

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

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

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

69 

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

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

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

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

74 """ 

75 

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

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

78 tabsize: int = 8, 

79 ambiguous_width: int = 1, 

80 **kwargs: Any) -> None: 

81 """ 

82 Initialize the wrapper. 

83 

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

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

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

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

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

89 """ 

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

91 self.control_codes = control_codes 

92 self.tabsize = tabsize 

93 self.ambiguous_width = ambiguous_width 

94 

95 @staticmethod 

96 def _next_hyperlink_id() -> str: 

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

98 return secrets.token_hex(4) 

99 

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

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

102 return _width(text, control_codes=self.control_codes, tabsize=self.tabsize, 

103 ambiguous_width=self.ambiguous_width) 

104 

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

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

107 result = [] 

108 for segment, is_seq in iter_sequences(text): 

109 if not is_seq: 

110 result.append(segment) 

111 return ''.join(result) 

112 

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

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

115 result = [] 

116 for segment, is_seq in iter_sequences(text): 

117 if is_seq: 

118 result.append(segment) 

119 return ''.join(result) 

120 

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

122 r""" 

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

124 

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

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

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

128 back. 

129 

130 OSC hyperlink sequences are treated as word boundaries:: 

131 

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

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

134 

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

136 """ 

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

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

139 # 

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

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

142 # aren't lost when whitespace is dropped. 

143 # 

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

145 char_end: list[int] = [] 

146 stripped_text = '' 

147 original_pos = 0 

148 prev_was_hyperlink_close = False 

149 

150 for segment, is_seq in iter_sequences(text): 

151 if not is_seq: 

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

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

154 stripped_text += ' ' 

155 char_end.append(original_pos) 

156 for char in segment: 

157 original_pos += 1 

158 char_end.append(original_pos) 

159 stripped_text += char 

160 prev_was_hyperlink_close = False 

161 else: 

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

163 

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

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

166 # terminated on the same line. 

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

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

169 if not is_hyperlink_close: 

170 stripped_text += ' ' 

171 char_end.append(original_pos) 

172 

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

174 original_pos += len(segment) 

175 prev_was_hyperlink_close = is_hyperlink_close 

176 

177 # Add sentinel for final position 

178 char_end.append(original_pos) 

179 

180 # Use parent's _split on the stripped text 

181 # pylint: disable-next=protected-access 

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

183 

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

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

186 if not stripped_chunks and text: 

187 return [text] 

188 

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

190 result: list[str] = [] 

191 stripped_pos = 0 

192 num_chunks = len(stripped_chunks) 

193 

194 for idx, chunk in enumerate(stripped_chunks): 

195 chunk_len = len(chunk) 

196 

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

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

199 

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

201 # to include any trailing sequences. 

202 if idx == num_chunks - 1: 

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

204 else: 

205 end_orig = char_end[stripped_pos + chunk_len - 1] 

206 

207 # Extract the corresponding portion from the original text 

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

209 if start_orig != end_orig: 

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

211 stripped_pos += chunk_len 

212 

213 return result 

214 

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

216 """ 

217 Wrap chunks into lines using sequence-aware width. 

218 

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

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

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

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

223 """ 

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

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

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

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

228 if not chunks: 

229 return [] 

230 

231 if self.max_lines is not None: 

232 if self.max_lines > 1: 

233 indent = self.subsequent_indent 

234 else: 

235 indent = self.initial_indent 

236 if (self._width(indent) 

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

238 > self.width): 

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

240 

241 lines: list[str] = [] 

242 is_first_line = True 

243 

244 hyperlink_state: _HyperlinkState | None = None 

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

246 current_hyperlink_id: str | None = None 

247 

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

249 chunks = list(reversed(chunks)) 

250 

251 while chunks: 

252 current_line: list[str] = [] 

253 current_width = 0 

254 

255 # Get the indent and available width for current line 

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

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

258 

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

260 if hyperlink_state is not None: 

261 open_seq = _make_hyperlink_open( 

262 hyperlink_state.url, hyperlink_state.params, hyperlink_state.terminator) 

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

264 

265 # Drop leading whitespace (except at very start) 

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

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

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

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

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

271 del chunks[-1] 

272 if sequences and chunks: 

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

274 

275 # Greedily add chunks that fit 

276 while chunks: 

277 chunk = chunks[-1] 

278 chunk_width = self._width(chunk) 

279 

280 if current_width + chunk_width <= line_width: 

281 current_line.append(chunks.pop()) 

282 current_width += chunk_width 

283 else: 

284 break 

285 

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

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

288 self._handle_long_word( 

289 chunks, current_line, current_width, line_width 

290 ) 

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

292 # Remove any empty chunks left by _handle_long_word 

293 while chunks and not chunks[-1]: 

294 del chunks[-1] 

295 

296 # Drop trailing whitespace 

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

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

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

300 if (self.drop_whitespace and current_line and 

301 stripped_last and not stripped_last.strip()): 

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

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

304 del current_line[-1] 

305 if sequences and current_line: 

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

307 

308 if current_line: 

309 # Check whether this is a normal append or max_lines 

310 # truncation. Matches stdlib textwrap precedence: 

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

312 # remaining visible content that would need truncation. 

313 no_more_content = ( 

314 not chunks or 

315 self.drop_whitespace and 

316 len(chunks) == 1 and 

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

318 ) 

319 if (self.max_lines is None or 

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

321 no_more_content 

322 and current_width <= line_width): 

323 line_content = ''.join(current_line) 

324 

325 # Track hyperlink state through this line's content 

326 new_state = self._track_hyperlink_state(line_content, hyperlink_state) 

327 

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

329 if new_state is not None: 

330 # Ensure we have an id for continuation 

331 if current_hyperlink_id is None: 

332 if 'id=' in new_state.params: 

333 current_hyperlink_id = new_state.params 

334 elif new_state.params: 

335 # Prepend id to existing params (per OSC 8 spec, params can have 

336 # multiple key=value pairs separated by :) 

337 current_hyperlink_id = ( 

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

339 else: 

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

341 line_content += _make_hyperlink_close(new_state.terminator) 

342 

343 # Also need to inject the id into the opening 

344 # sequence if it didn't have one 

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

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

347 old_open = _make_hyperlink_open( 

348 new_state.url, new_state.params, new_state.terminator) 

349 new_open = _make_hyperlink_open( 

350 new_state.url, current_hyperlink_id, new_state.terminator) 

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

352 

353 # Update state for next line, using computed id 

354 hyperlink_state = _HyperlinkState( 

355 new_state.url, current_hyperlink_id, new_state.terminator) 

356 else: 

357 hyperlink_state = None 

358 current_hyperlink_id = None # Reset id when hyperlink closes 

359 

360 # Strip trailing whitespace when drop_whitespace is enabled 

361 # (matches CPython #140627 fix behavior) 

362 if self.drop_whitespace: 

363 line_content = line_content.rstrip() 

364 lines.append(indent + line_content) 

365 is_first_line = False 

366 else: 

367 # max_lines reached with remaining content — 

368 # pop chunks until placeholder fits, then break. 

369 placeholder_w = self._width(self.placeholder) 

370 while current_line: 

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

372 if (last_text.strip() 

373 and current_width + placeholder_w <= line_width): 

374 line_content = ''.join(current_line) 

375 new_state = self._track_hyperlink_state( 

376 line_content, hyperlink_state) 

377 if new_state is not None: 

378 line_content += _make_hyperlink_close( 

379 new_state.terminator) 

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

381 break 

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

383 del current_line[-1] 

384 else: 

385 if lines: 

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

387 if (self._width(prev_line) + placeholder_w 

388 <= self.width): 

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

390 break 

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

392 break 

393 

394 return lines 

395 

396 def _track_hyperlink_state( 

397 self, text: str, 

398 state: _HyperlinkState | None) -> _HyperlinkState | None: 

399 """ 

400 Track hyperlink state through text. 

401 

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

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

404 :returns: Updated state after processing text. 

405 """ 

406 for segment, is_seq in iter_sequences(text): 

407 if is_seq: 

408 parsed_link = _parse_hyperlink_open(segment) 

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

410 state = parsed_link 

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

412 state = None 

413 return state 

414 

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

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

417 width: int) -> None: 

418 """ 

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

420 

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

422 cluster boundaries when breaking long words. 

423 """ 

424 if width < 1: 

425 space_left = 1 

426 else: 

427 space_left = width - cur_len 

428 

429 chunk = reversed_chunks[-1] 

430 

431 if self.break_long_words: 

432 break_at_hyphen = False 

433 hyphen_end = 0 

434 

435 # Handle break_on_hyphens: find last hyphen within space_left 

436 if self.break_on_hyphens: 

437 # Strip sequences to find hyphen in logical text 

438 stripped = self._strip_sequences(chunk) 

439 if len(stripped) > space_left: 

440 # Find last hyphen in the portion that fits 

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

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 = self._find_break_position(chunk, space_left) 

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

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

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

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

456 if actual_end == 0 and not cur_line: 

457 actual_end = self._find_first_grapheme_end(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_grapheme_end(self, text: str) -> int: 

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

514 return len(next(iter_graphemes(text))) 

515 

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

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

518 segments = list(iter_sequences(text)) 

519 last_vis = -1 

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

521 if not is_seq and segment.rstrip(): 

522 last_vis = i 

523 if last_vis == -1: 

524 return '' 

525 result = [] 

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

527 if i < last_vis: 

528 result.append(segment) 

529 elif i == last_vis: 

530 result.append(segment.rstrip()) 

531 elif is_seq: 

532 result.append(segment) 

533 return ''.join(result) 

534 

535 

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

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

538 tabsize: int = 8, 

539 expand_tabs: bool = True, 

540 replace_whitespace: bool = True, 

541 ambiguous_width: int = 1, 

542 initial_indent: str = '', 

543 subsequent_indent: str = '', 

544 fix_sentence_endings: bool = False, 

545 break_long_words: bool = True, 

546 break_on_hyphens: bool = True, 

547 drop_whitespace: bool = True, 

548 max_lines: int | None = None, 

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

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

551 r""" 

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

553 

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

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

556 escape sequences. 

557 

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

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

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

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

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

563 to spaces using ``tabsize``. 

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

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

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

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

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

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

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

571 :param initial_indent: String prepended to first line. 

572 :param subsequent_indent: String prepended to subsequent lines. 

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

574 separated by exactly two spaces. 

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

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

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

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

579 Set to False to preserve whitespace. 

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

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

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

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

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

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

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

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

588 

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

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

591 begins with the active style restored:: 

592 

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

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

595 

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

597 

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

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

600 paragraph separately:: 

601 

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

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

604 ['First line. Second line.'] 

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

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

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

608 

609 .. seealso:: 

610 

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

612 Standard library text wrapping (character-based). 

613 

614 :class:`.SequenceTextWrapper` 

615 Class interface for advanced wrapping options. 

616 

617 .. versionadded:: 0.3.0 

618 

619 .. versionchanged:: 0.5.0 

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

621 

622 .. versionchanged:: 0.6.0 

623 Added ``expand_tabs``, ``replace_whitespace``, ``fix_sentence_endings``, 

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

625 

626 Example:: 

627 

628 >>> from wcwidth import wrap 

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

630 ['hello', 'world'] 

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

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

633 """ 

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

635 wrapper = SequenceTextWrapper( 

636 width=width, 

637 control_codes=control_codes, 

638 tabsize=tabsize, 

639 expand_tabs=expand_tabs, 

640 replace_whitespace=replace_whitespace, 

641 ambiguous_width=ambiguous_width, 

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