Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/click/formatting.py: 14%

138 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1import typing as t 

2from contextlib import contextmanager 

3from gettext import gettext as _ 

4 

5from ._compat import term_len 

6from .parser import split_opt 

7 

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

9FORCED_WIDTH: t.Optional[int] = None 

10 

11 

12def measure_table(rows: t.Iterable[t.Tuple[str, str]]) -> t.Tuple[int, ...]: 

13 widths: t.Dict[int, int] = {} 

14 

15 for row in rows: 

16 for idx, col in enumerate(row): 

17 widths[idx] = max(widths.get(idx, 0), term_len(col)) 

18 

19 return tuple(y for x, y in sorted(widths.items())) 

20 

21 

22def iter_rows( 

23 rows: t.Iterable[t.Tuple[str, str]], col_count: int 

24) -> t.Iterator[t.Tuple[str, ...]]: 

25 for row in rows: 

26 yield row + ("",) * (col_count - len(row)) 

27 

28 

29def wrap_text( 

30 text: str, 

31 width: int = 78, 

32 initial_indent: str = "", 

33 subsequent_indent: str = "", 

34 preserve_paragraphs: bool = False, 

35) -> str: 

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

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

38 `preserve_paragraphs` parameter is provided it will intelligently 

39 handle paragraphs (defined by two empty lines). 

40 

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

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

43 no rewrapping should happen in that block. 

44 

45 :param text: the text that should be rewrapped. 

46 :param width: the maximum width for the text. 

47 :param initial_indent: the initial indent that should be placed on the 

48 first line as a string. 

49 :param subsequent_indent: the indent string that should be placed on 

50 each consecutive line. 

51 :param preserve_paragraphs: if this flag is set then the wrapping will 

52 intelligently handle paragraphs. 

53 """ 

54 from ._textwrap import TextWrapper 

55 

56 text = text.expandtabs() 

57 wrapper = TextWrapper( 

58 width, 

59 initial_indent=initial_indent, 

60 subsequent_indent=subsequent_indent, 

61 replace_whitespace=False, 

62 ) 

63 if not preserve_paragraphs: 

64 return wrapper.fill(text) 

65 

66 p: t.List[t.Tuple[int, bool, str]] = [] 

67 buf: t.List[str] = [] 

68 indent = None 

69 

70 def _flush_par() -> None: 

71 if not buf: 

72 return 

73 if buf[0].strip() == "\b": 

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

75 else: 

76 p.append((indent or 0, False, " ".join(buf))) 

77 del buf[:] 

78 

79 for line in text.splitlines(): 

80 if not line: 

81 _flush_par() 

82 indent = None 

83 else: 

84 if indent is None: 

85 orig_len = term_len(line) 

86 line = line.lstrip() 

87 indent = orig_len - term_len(line) 

88 buf.append(line) 

89 _flush_par() 

90 

91 rv = [] 

92 for indent, raw, text in p: 

93 with wrapper.extra_indent(" " * indent): 

94 if raw: 

95 rv.append(wrapper.indent_only(text)) 

96 else: 

97 rv.append(wrapper.fill(text)) 

98 

99 return "\n\n".join(rv) 

100 

101 

102class HelpFormatter: 

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

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

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

106 

107 At present, it always writes into memory. 

108 

109 :param indent_increment: the additional increment for each level. 

110 :param width: the width for the text. This defaults to the terminal 

111 width clamped to a maximum of 78. 

112 """ 

113 

114 def __init__( 

115 self, 

116 indent_increment: int = 2, 

117 width: t.Optional[int] = None, 

118 max_width: t.Optional[int] = None, 

119 ) -> None: 

120 import shutil 

121 

122 self.indent_increment = indent_increment 

123 if max_width is None: 

124 max_width = 80 

125 if width is None: 

126 width = FORCED_WIDTH 

127 if width is None: 

128 width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50) 

129 self.width = width 

130 self.current_indent = 0 

131 self.buffer: t.List[str] = [] 

132 

133 def write(self, string: str) -> None: 

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

135 self.buffer.append(string) 

136 

137 def indent(self) -> None: 

138 """Increases the indentation.""" 

139 self.current_indent += self.indent_increment 

140 

141 def dedent(self) -> None: 

142 """Decreases the indentation.""" 

143 self.current_indent -= self.indent_increment 

144 

145 def write_usage( 

146 self, prog: str, args: str = "", prefix: t.Optional[str] = None 

147 ) -> None: 

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

149 

150 :param prog: the program name. 

151 :param args: whitespace separated list of arguments. 

152 :param prefix: The prefix for the first line. Defaults to 

153 ``"Usage: "``. 

154 """ 

155 if prefix is None: 

156 prefix = f"{_('Usage:')} " 

157 

158 usage_prefix = f"{prefix:>{self.current_indent}}{prog} " 

159 text_width = self.width - self.current_indent 

160 

161 if text_width >= (term_len(usage_prefix) + 20): 

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

163 indent = " " * term_len(usage_prefix) 

164 self.write( 

165 wrap_text( 

166 args, 

167 text_width, 

168 initial_indent=usage_prefix, 

169 subsequent_indent=indent, 

170 ) 

171 ) 

172 else: 

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

174 self.write(usage_prefix) 

175 self.write("\n") 

176 indent = " " * (max(self.current_indent, term_len(prefix)) + 4) 

177 self.write( 

178 wrap_text( 

179 args, text_width, initial_indent=indent, subsequent_indent=indent 

180 ) 

181 ) 

182 

183 self.write("\n") 

184 

185 def write_heading(self, heading: str) -> None: 

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

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

188 

189 def write_paragraph(self) -> None: 

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

191 if self.buffer: 

192 self.write("\n") 

193 

194 def write_text(self, text: str) -> None: 

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

196 preserves paragraphs. 

197 """ 

198 indent = " " * self.current_indent 

199 self.write( 

200 wrap_text( 

201 text, 

202 self.width, 

203 initial_indent=indent, 

204 subsequent_indent=indent, 

205 preserve_paragraphs=True, 

206 ) 

207 ) 

208 self.write("\n") 

209 

210 def write_dl( 

211 self, 

212 rows: t.Sequence[t.Tuple[str, str]], 

213 col_max: int = 30, 

214 col_spacing: int = 2, 

215 ) -> None: 

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

217 and commands are usually formatted. 

218 

219 :param rows: a list of two item tuples for the terms and values. 

220 :param col_max: the maximum width of the first column. 

221 :param col_spacing: the number of spaces between the first and 

222 second column. 

223 """ 

224 rows = list(rows) 

225 widths = measure_table(rows) 

226 if len(widths) != 2: 

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

228 

229 first_col = min(widths[0], col_max) + col_spacing 

230 

231 for first, second in iter_rows(rows, len(widths)): 

232 self.write(f"{'':>{self.current_indent}}{first}") 

233 if not second: 

234 self.write("\n") 

235 continue 

236 if term_len(first) <= first_col - col_spacing: 

237 self.write(" " * (first_col - term_len(first))) 

238 else: 

239 self.write("\n") 

240 self.write(" " * (first_col + self.current_indent)) 

241 

242 text_width = max(self.width - first_col - 2, 10) 

243 wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True) 

244 lines = wrapped_text.splitlines() 

245 

246 if lines: 

247 self.write(f"{lines[0]}\n") 

248 

249 for line in lines[1:]: 

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

251 else: 

252 self.write("\n") 

253 

254 @contextmanager 

255 def section(self, name: str) -> t.Iterator[None]: 

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

257 and the indents. 

258 

259 :param name: the section name that is written as heading. 

260 """ 

261 self.write_paragraph() 

262 self.write_heading(name) 

263 self.indent() 

264 try: 

265 yield 

266 finally: 

267 self.dedent() 

268 

269 @contextmanager 

270 def indentation(self) -> t.Iterator[None]: 

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

272 self.indent() 

273 try: 

274 yield 

275 finally: 

276 self.dedent() 

277 

278 def getvalue(self) -> str: 

279 """Returns the buffer contents.""" 

280 return "".join(self.buffer) 

281 

282 

283def join_options(options: t.Sequence[str]) -> t.Tuple[str, bool]: 

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

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

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

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

288 """ 

289 rv = [] 

290 any_prefix_is_slash = False 

291 

292 for opt in options: 

293 prefix = split_opt(opt)[0] 

294 

295 if prefix == "/": 

296 any_prefix_is_slash = True 

297 

298 rv.append((len(prefix), opt)) 

299 

300 rv.sort(key=lambda x: x[0]) 

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