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

103 statements  

1from __future__ import annotations 

2 

3import collections.abc as cabc 

4import textwrap 

5from contextlib import contextmanager 

6 

7from ._compat import _ansi_re 

8from ._compat import term_len 

9 

10 

11def _truncate_visible(text: str, n: int) -> str: 

12 """Return the longest prefix of ``text`` containing at most ``n`` visible 

13 characters. 

14 

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

20 

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] 

36 

37 

38class TextWrapper(textwrap.TextWrapper): 

39 """``textwrap.TextWrapper`` variant that measures widths by visible 

40 character count. 

41 

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

47 

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) 

56 

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

65 

66 def _wrap_chunks(self, chunks: list[str]) -> list[str]: 

67 """Wrap chunks counting widths in visible characters. 

68 

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. 

73 

74 .. seealso:: 

75 :class:`textwrap.TextWrapper` in the Python standard library documentation: 

76 https://docs.python.org/3/library/textwrap.html#textwrap.TextWrapper 

77 

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

91 

92 chunks.reverse() 

93 

94 while chunks: 

95 cur_line: list[str] = [] 

96 cur_len = 0 

97 

98 if lines: 

99 indent = self.subsequent_indent 

100 else: 

101 indent = self.initial_indent 

102 

103 width = self.width - term_len(indent) 

104 

105 if self.drop_whitespace and chunks[-1].strip() == "" and lines: 

106 del chunks[-1] 

107 

108 while chunks: 

109 n = term_len(chunks[-1]) 

110 

111 if cur_len + n <= width: 

112 cur_line.append(chunks.pop()) 

113 cur_len += n 

114 

115 else: 

116 break 

117 

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

121 

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] 

125 

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 

161 

162 return lines 

163 

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 

170 

171 try: 

172 yield 

173 finally: 

174 self.initial_indent = old_initial_indent 

175 self.subsequent_indent = old_subsequent_indent 

176 

177 def indent_only(self, text: str) -> str: 

178 rv = [] 

179 

180 for idx, line in enumerate(text.splitlines()): 

181 indent = self.initial_indent 

182 

183 if idx > 0: 

184 indent = self.subsequent_indent 

185 

186 rv.append(f"{indent}{line}") 

187 

188 return "\n".join(rv)