Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/click/_textwrap.py: 0%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import collections.abc as cabc
4import textwrap
5from contextlib import contextmanager
7from ._compat import _ansi_re
8from ._compat import term_len
11def _truncate_visible(text: str, n: int) -> str:
12 """Return the longest prefix of ``text`` containing at most ``n`` visible
13 characters.
15 ANSI escape sequences inside the prefix are kept intact and do not count
16 toward the visible width. A cut is never placed inside an escape sequence.
17 """
18 if n <= 0:
19 return ""
21 visible = 0
22 i = 0
23 cut = 0
24 end = len(text)
25 while i < end:
26 m = _ansi_re.match(text, i)
27 if m is not None:
28 i = m.end()
29 continue
30 visible += 1
31 i += 1
32 cut = i
33 if visible >= n:
34 break
35 return text[:cut]
38class TextWrapper(textwrap.TextWrapper):
39 """``textwrap.TextWrapper`` variant that measures widths by visible
40 character count.
42 ANSI escape sequences embedded in chunks, indents, or the placeholder are
43 excluded from the width budget. Without this, styled help text (a styled
44 ``Usage:`` prefix, a colorized option name, ...) would be wrapped earlier
45 than its visible length warrants and tokens would split mid-word.
46 """
48 def _handle_long_word(
49 self,
50 reversed_chunks: list[str],
51 cur_line: list[str],
52 cur_len: int,
53 width: int,
54 ) -> None:
55 space_left = max(width - cur_len, 1)
57 if self.break_long_words:
58 last = reversed_chunks[-1]
59 cut = _truncate_visible(last, space_left)
60 res = last[len(cut) :]
61 cur_line.append(cut)
62 reversed_chunks[-1] = res
63 elif not cur_line:
64 cur_line.append(reversed_chunks.pop())
66 def _wrap_chunks(self, chunks: list[str]) -> list[str]:
67 """Wrap chunks counting widths in visible characters.
69 Mirrors the algorithm of :meth:`textwrap.TextWrapper._wrap_chunks`
70 with every width measurement routed through
71 :func:`click._compat.term_len` instead of :func:`len`, so ANSI escape
72 bytes in chunks, indents, or the placeholder do not inflate the count.
74 .. seealso::
75 :class:`textwrap.TextWrapper` in the Python standard library documentation:
76 https://docs.python.org/3/library/textwrap.html#textwrap.TextWrapper
78 Reference implementation in CPython:
79 https://github.com/python/cpython/blob/main/Lib/textwrap.py
80 """
81 lines: list[str] = []
82 if self.width <= 0:
83 raise ValueError(f"invalid width {self.width!r} (must be > 0)")
84 if self.max_lines is not None:
85 if self.max_lines > 1:
86 indent = self.subsequent_indent
87 else:
88 indent = self.initial_indent
89 if term_len(indent) + term_len(self.placeholder.lstrip()) > self.width:
90 raise ValueError("placeholder too large for max width")
92 chunks.reverse()
94 while chunks:
95 cur_line: list[str] = []
96 cur_len = 0
98 if lines:
99 indent = self.subsequent_indent
100 else:
101 indent = self.initial_indent
103 width = self.width - term_len(indent)
105 if self.drop_whitespace and chunks[-1].strip() == "" and lines:
106 del chunks[-1]
108 while chunks:
109 n = term_len(chunks[-1])
111 if cur_len + n <= width:
112 cur_line.append(chunks.pop())
113 cur_len += n
115 else:
116 break
118 if chunks and term_len(chunks[-1]) > width:
119 self._handle_long_word(chunks, cur_line, cur_len, width)
120 cur_len = sum(map(term_len, cur_line))
122 if self.drop_whitespace and cur_line and cur_line[-1].strip() == "":
123 cur_len -= term_len(cur_line[-1])
124 del cur_line[-1]
126 if cur_line:
127 if (
128 self.max_lines is None
129 or len(lines) + 1 < self.max_lines
130 or (
131 not chunks
132 or self.drop_whitespace
133 and len(chunks) == 1
134 and not chunks[0].strip()
135 )
136 and cur_len <= width
137 ):
138 lines.append(indent + "".join(cur_line))
139 else:
140 while cur_line:
141 if (
142 cur_line[-1].strip()
143 and cur_len + term_len(self.placeholder) <= width
144 ):
145 cur_line.append(self.placeholder)
146 lines.append(indent + "".join(cur_line))
147 break
148 cur_len -= term_len(cur_line[-1])
149 del cur_line[-1]
150 else:
151 if lines:
152 prev_line = lines[-1].rstrip()
153 if (
154 term_len(prev_line) + term_len(self.placeholder)
155 <= self.width
156 ):
157 lines[-1] = prev_line + self.placeholder
158 break
159 lines.append(indent + self.placeholder.lstrip())
160 break
162 return lines
164 @contextmanager
165 def extra_indent(self, indent: str) -> cabc.Iterator[None]:
166 old_initial_indent = self.initial_indent
167 old_subsequent_indent = self.subsequent_indent
168 self.initial_indent += indent
169 self.subsequent_indent += indent
171 try:
172 yield
173 finally:
174 self.initial_indent = old_initial_indent
175 self.subsequent_indent = old_subsequent_indent
177 def indent_only(self, text: str) -> str:
178 rv = []
180 for idx, line in enumerate(text.splitlines()):
181 indent = self.initial_indent
183 if idx > 0:
184 indent = self.subsequent_indent
186 rv.append(f"{indent}{line}")
188 return "\n".join(rv)