Coverage for typer / _click / formatting.py: 100%

134 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-09 00:15 +0000

1from collections.abc import Generator, Iterable, Iterator, Sequence 1abcdefg

2from contextlib import contextmanager 1abcdefg

3 

4from ._compat import term_len 1abcdefg

5from .parser import _split_opt 1abcdefg

6 

7# Can force a width. This is used by the test system 

8FORCED_WIDTH: int | None = None 1abcdefg

9 

10 

11def measure_table(rows: Iterable[tuple[str, str]]) -> tuple[int, ...]: 1abcdefg

12 widths: dict[int, int] = {} 1abcdefg

13 

14 for row in rows: 1abcdefg

15 for idx, col in enumerate(row): 1abcdefg

16 widths[idx] = max(widths.get(idx, 0), term_len(col)) 1abcdefg

17 

18 return tuple(y for x, y in sorted(widths.items())) 1abcdefg

19 

20 

21def iter_rows( 1abcdefg

22 rows: Iterable[tuple[str, str]], col_count: int 

23) -> Iterator[tuple[str, ...]]: 

24 for row in rows: 1abcdefg

25 yield row + ("",) * (col_count - len(row)) 1abcdefg

26 

27 

28def wrap_text( 1abcdefg

29 text: str, 

30 width: int = 78, 

31 initial_indent: str = "", 

32 subsequent_indent: str = "", 

33 preserve_paragraphs: bool = False, 

34) -> str: 

35 """A helper function that intelligently wraps text. By default, it 

36 assumes that it operates on a single paragraph of text but if the 

37 `preserve_paragraphs` parameter is provided it will intelligently 

38 handle paragraphs (defined by two empty lines). 

39 

40 If paragraphs are handled, a paragraph can be prefixed with an empty 

41 line containing the ``\\b`` character (``\\x08``) to indicate that 

42 no rewrapping should happen in that block. 

43 """ 

44 from ._textwrap import TextWrapper 1abcdefg

45 

46 text = text.expandtabs() 1abcdefg

47 wrapper = TextWrapper( 1abcdefg

48 width, 

49 initial_indent=initial_indent, 

50 subsequent_indent=subsequent_indent, 

51 replace_whitespace=False, 

52 ) 

53 if not preserve_paragraphs: 1abcdefg

54 return wrapper.fill(text) 1abcdefg

55 

56 p: list[tuple[int, bool, str]] = [] 1abcdefg

57 buf: list[str] = [] 1abcdefg

58 indent = None 1abcdefg

59 

60 def _flush_par() -> None: 1abcdefg

61 if not buf: 1abcdefg

62 return 1abcdefg

63 if buf[0].strip() == "\b": 1abcdefg

64 p.append((indent or 0, True, "\n".join(buf[1:]))) 1abcdefg

65 else: 

66 p.append((indent or 0, False, " ".join(buf))) 1abcdefg

67 del buf[:] 1abcdefg

68 

69 for line in text.splitlines(): 1abcdefg

70 if not line: 1abcdefg

71 _flush_par() 1abcdefg

72 indent = None 1abcdefg

73 else: 

74 if indent is None: 1abcdefg

75 orig_len = term_len(line) 1abcdefg

76 line = line.lstrip() 1abcdefg

77 indent = orig_len - term_len(line) 1abcdefg

78 buf.append(line) 1abcdefg

79 _flush_par() 1abcdefg

80 

81 rv = [] 1abcdefg

82 for indent, raw, text in p: 1abcdefg

83 with wrapper.extra_indent(" " * indent): 1abcdefg

84 if raw: 1abcdefg

85 rv.append(wrapper.indent_only(text)) 1abcdefg

86 else: 

87 rv.append(wrapper.fill(text)) 1abcdefg

88 

89 return "\n\n".join(rv) 1abcdefg

90 

91 

92class HelpFormatter: 1abcdefg

93 """This class helps with formatting text-based help pages. It's 

94 usually just needed for very special internal cases, but it's also 

95 exposed so that developers can write their own fancy outputs. 

96 

97 At present, it always writes into memory. 

98 """ 

99 

100 def __init__( 1abcdefg

101 self, 

102 indent_increment: int = 2, 

103 width: int | None = None, 

104 max_width: int | None = None, 

105 ) -> None: 

106 self.indent_increment = indent_increment 1abcdefg

107 if max_width is None: 1abcdefg

108 max_width = 80 1abcdefg

109 if width is None: 1abcdefg

110 import shutil 1abcdefg

111 

112 width = FORCED_WIDTH 1abcdefg

113 if width is None: 1abcdefg

114 width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50) 1abcdefg

115 self.width = width 1abcdefg

116 self.current_indent: int = 0 1abcdefg

117 self.buffer: list[str] = [] 1abcdefg

118 

119 def write(self, string: str) -> None: 1abcdefg

120 """Writes a unicode string into the internal buffer.""" 

121 self.buffer.append(string) 1abcdefg

122 

123 def indent(self) -> None: 1abcdefg

124 """Increases the indentation.""" 

125 self.current_indent += self.indent_increment 1abcdefg

126 

127 def dedent(self) -> None: 1abcdefg

128 """Decreases the indentation.""" 

129 self.current_indent -= self.indent_increment 1abcdefg

130 

131 def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> None: 1abcdefg

132 """Writes a usage line into the buffer.""" 

133 if prefix is None: 1abcdefg

134 prefix = "Usage: " 1abcdefg

135 

136 usage_prefix = f"{prefix:>{self.current_indent}}{prog} " 1abcdefg

137 text_width = self.width - self.current_indent 1abcdefg

138 

139 if text_width >= (term_len(usage_prefix) + 20): 1abcdefg

140 # The arguments will fit to the right of the prefix. 

141 indent = " " * term_len(usage_prefix) 1abcdefg

142 self.write( 1abcdefg

143 wrap_text( 

144 args, 

145 text_width, 

146 initial_indent=usage_prefix, 

147 subsequent_indent=indent, 

148 ) 

149 ) 

150 else: 

151 # The prefix is too long, put the arguments on the next line. 

152 self.write(usage_prefix) 1abcdefg

153 self.write("\n") 1abcdefg

154 indent = " " * (max(self.current_indent, term_len(prefix)) + 4) 1abcdefg

155 self.write( 1abcdefg

156 wrap_text( 

157 args, text_width, initial_indent=indent, subsequent_indent=indent 

158 ) 

159 ) 

160 

161 self.write("\n") 1abcdefg

162 

163 def write_heading(self, heading: str) -> None: 1abcdefg

164 """Writes a heading into the buffer.""" 

165 self.write(f"{'':>{self.current_indent}}{heading}:\n") 1abcdefg

166 

167 def write_paragraph(self) -> None: 1abcdefg

168 """Writes a paragraph into the buffer.""" 

169 if self.buffer: 1abcdefg

170 self.write("\n") 1abcdefg

171 

172 def write_text(self, text: str) -> None: 1abcdefg

173 """Writes re-indented text into the buffer. This rewraps and 

174 preserves paragraphs. 

175 """ 

176 indent = " " * self.current_indent 1abcdefg

177 self.write( 1abcdefg

178 wrap_text( 

179 text, 

180 self.width, 

181 initial_indent=indent, 

182 subsequent_indent=indent, 

183 preserve_paragraphs=True, 

184 ) 

185 ) 

186 self.write("\n") 1abcdefg

187 

188 def write_dl( 1abcdefg

189 self, 

190 rows: Sequence[tuple[str, str]], 

191 col_max: int = 30, 

192 col_spacing: int = 2, 

193 ) -> None: 

194 """Writes a definition list into the buffer. This is how options 

195 and commands are usually formatted. 

196 """ 

197 rows = list(rows) 1abcdefg

198 widths = measure_table(rows) 1abcdefg

199 if len(widths) != 2: # pragma: no cover 1abcdefg

200 raise TypeError("Expected two columns for definition list") 

201 

202 first_col = min(widths[0], col_max) + col_spacing 1abcdefg

203 

204 for first, second in iter_rows(rows, len(widths)): 1abcdefg

205 self.write(f"{'':>{self.current_indent}}{first}") 1abcdefg

206 if not second: 1abcdefg

207 self.write("\n") 1abcdefg

208 continue 1abcdefg

209 if term_len(first) <= first_col - col_spacing: 1abcdefg

210 self.write(" " * (first_col - term_len(first))) 1abcdefg

211 else: 

212 self.write("\n") 1abcdefg

213 self.write(" " * (first_col + self.current_indent)) 1abcdefg

214 

215 text_width = max(self.width - first_col - 2, 10) 1abcdefg

216 wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True) 1abcdefg

217 lines = wrapped_text.splitlines() 1abcdefg

218 

219 if lines: 1abcdefg

220 self.write(f"{lines[0]}\n") 1abcdefg

221 

222 for line in lines[1:]: 1abcdefg

223 self.write(f"{'':>{first_col + self.current_indent}}{line}\n") 1abcdefg

224 else: # pragma: no cover 

225 self.write("\n") 

226 

227 @contextmanager 1abcdefg

228 def section(self, name: str) -> Generator[None]: 1abcdefg

229 """Helpful context manager that writes a paragraph, a heading, 

230 and the indents. 

231 """ 

232 self.write_paragraph() 1abcdefg

233 self.write_heading(name) 1abcdefg

234 self.indent() 1abcdefg

235 try: 1abcdefg

236 yield 1abcdefg

237 finally: 

238 self.dedent() 1abcdefg

239 

240 @contextmanager 1abcdefg

241 def indentation(self) -> Generator[None]: 1abcdefg

242 """A context manager that increases the indentation.""" 

243 self.indent() 1abcdefg

244 try: 1abcdefg

245 yield 1abcdefg

246 finally: 

247 self.dedent() 1abcdefg

248 

249 def getvalue(self) -> str: 1abcdefg

250 """Returns the buffer contents.""" 

251 return "".join(self.buffer) 1abcdefg

252 

253 

254def join_options(options: Sequence[str]) -> tuple[str, bool]: 1abcdefg

255 """Given a list of option strings this joins them in the most appropriate 

256 way and returns them in the form ``(formatted_string, 

257 any_prefix_is_slash)`` where the second item in the tuple is a flag that 

258 indicates if any of the option prefixes was a slash. 

259 """ 

260 rv = [] 1abcdefg

261 any_prefix_is_slash = False 1abcdefg

262 

263 for opt in options: 1abcdefg

264 prefix = _split_opt(opt)[0] 1abcdefg

265 

266 if prefix == "/": 1abcdefg

267 any_prefix_is_slash = True 1abcdefg

268 

269 rv.append((len(prefix), opt)) 1abcdefg

270 

271 rv.sort(key=lambda x: x[0]) 1abcdefg

272 return ", ".join(x[1] for x in rv), any_prefix_is_slash 1abcdefg