Coverage for argparse_color_formatter.py: 86%

201 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-03-18 14:53 +0000

1# -*- coding: utf-8 -*- 

2# original implementations of the methods below are 

3# Copyright © 2001-2017 Python Software Foundation; All Rights Reserved 

4# they are licensed under the PSF LICENSE AGREEMENT FOR PYTHON 3.5.4 

5 

6# changes were applied to allow these methods to deal with ansi color escape codes. These changes are 

7# Copyright (c) 2017, Emergence by Design Inc. 

8# Copyright (c) 2024, Arrai Innovations Inc. 

9 

10 

11__version__ = "2.0.0.post1" 

12import re as _re 

13from argparse import HelpFormatter 

14from argparse import SUPPRESS 

15from argparse import ArgumentDefaultsHelpFormatter 

16 

17try: 

18 from argparse import MetavarTypeHelpFormatter 

19except ImportError: 

20 pass 

21from argparse import RawDescriptionHelpFormatter 

22from argparse import RawTextHelpFormatter 

23from argparse import ZERO_OR_MORE 

24from gettext import gettext as _ 

25from textwrap import TextWrapper 

26 

27from colors import strip_color 

28 

29 

30def color_aware_pad(text, width, char=" "): 

31 return text + char * (width - len(strip_color(text))) 

32 

33 

34class ColorHelpFormatterMixin(object): 

35 def _fill_text(self, text, width, indent): 

36 text = self._whitespace_matcher.sub(" ", text).strip() 

37 return ColorTextWrapper(width=width, initial_indent=indent, subsequent_indent=indent).fill(text) 

38 

39 def _split_lines(self, text, width): 

40 text = self._whitespace_matcher.sub(" ", text).strip() 

41 return ColorTextWrapper(width=width).wrap(text) 

42 

43 def add_argument(self, action): 

44 old_max = self._action_max_length 

45 super(ColorHelpFormatterMixin, self).add_argument(action) 

46 # the self._action_max_length updated above won't account for color codes, 

47 # so we need to update it here as well 

48 if action.help is not SUPPRESS: 48 ↛ exitline 48 didn't return from function 'add_argument', because the condition on line 48 was never false

49 self._action_max_length = old_max 

50 get_invocation = self._format_action_invocation 

51 invocations = [get_invocation(action)] 

52 for subaction in self._iter_indented_subactions(action): 52 ↛ 53line 52 didn't jump to line 53, because the loop on line 52 never started

53 invocations.append(get_invocation(subaction)) 

54 

55 invocation_length = max(len(strip_color(invocation)) for invocation in invocations) 

56 action_length = invocation_length + self._current_indent 

57 self._action_max_length = max(self._action_max_length, action_length) 

58 

59 def _format_args(self, action, default_metavar): 

60 result = super()._format_args(action, default_metavar) 

61 if action.nargs == ZERO_OR_MORE: 61 ↛ 62line 61 didn't jump to line 62, because the condition on line 61 was never true

62 metavar = self._metavar_formatter(action, default_metavar)(1) 

63 if len(strip_color(metavar)) == 2: 

64 result = "[%s [%s ...]]" % metavar 

65 else: 

66 result = "[%s ...]" % metavar 

67 return result 

68 

69 # modified upstream code 

70 # fmt: off 

71 def _format_action(self, action): 

72 # determine the required width and the entry label 

73 help_position = min(self._action_max_length + 2, 

74 self._max_help_position) 

75 help_width = max(self._width - help_position, 11) 

76 action_width = help_position - self._current_indent - 2 

77 action_header = self._format_action_invocation(action) 

78 

79 # no help; start on same line and add a final newline 

80 if not action.help: 80 ↛ 81line 80 didn't jump to line 81, because the condition on line 80 was never true

81 tup = self._current_indent, '', action_header 

82 action_header = '%*s%s\n' % tup 

83 

84 # short action name; start on the same line and pad two spaces 

85 elif len(strip_color(action_header)) <= action_width: 85 ↛ 92line 85 didn't jump to line 92, because the condition on line 85 was never false

86 tup = self._current_indent, '', color_aware_pad(action_header, action_width) 

87 action_header = '%*s%s ' % tup 

88 indent_first = 0 

89 

90 # long action name; start on the next line 

91 else: 

92 tup = self._current_indent, '', action_header 

93 action_header = '%*s%s\n' % tup 

94 indent_first = help_position 

95 

96 # collect the pieces of the action help 

97 parts = [action_header] 

98 

99 # if there was help for the action, add lines of help text 

100 if action.help and action.help.strip(): 100 ↛ 109line 100 didn't jump to line 109, because the condition on line 100 was never false

101 help_text = self._expand_help(action) 

102 if help_text: 102 ↛ 113line 102 didn't jump to line 113, because the condition on line 102 was never false

103 help_lines = self._split_lines(help_text, help_width) 

104 parts.append('%*s%s\n' % (indent_first, '', help_lines[0])) 

105 for line in help_lines[1:]: 

106 parts.append('%*s%s\n' % (help_position, '', line)) 

107 

108 # or add a newline if the description doesn't end with one 

109 elif not action_header.endswith('\n'): 

110 parts.append('\n') 

111 

112 # if there are any sub-actions, add their help as well 

113 for subaction in self._iter_indented_subactions(action): 113 ↛ 114line 113 didn't jump to line 114, because the loop on line 113 never started

114 parts.append(self._format_action(subaction)) 

115 

116 # return a single string 

117 return self._join_parts(parts) 

118 # fmt: on 

119 

120 # modified upstream code, not going to refactor for complexity. 

121 # fmt: off 

122 def _format_usage(self, usage, actions, groups, prefix): # noqa: C901 

123 if prefix is None: 123 ↛ 125line 123 didn't jump to line 125, because the condition on line 123 was never false

124 prefix = _("usage: ") 

125 prefix_len = len(strip_color(prefix)) 

126 

127 # if usage is specified, use that 

128 if usage is not None: 

129 usage = usage % dict(prog=self._prog) 

130 

131 # if no optionals or positionals are available, usage is just prog 

132 elif usage is None and not actions: 

133 usage = "%(prog)s" % dict(prog=self._prog) 

134 

135 # if optionals and positionals are available, calculate usage 

136 elif usage is None: 136 ↛ 220line 136 didn't jump to line 220, because the condition on line 136 was never false

137 prog = "%(prog)s" % dict(prog=self._prog) 

138 

139 # split optionals from positionals 

140 optionals = [] 

141 positionals = [] 

142 for action in actions: 

143 if action.option_strings: 

144 optionals.append(action) 

145 else: 

146 positionals.append(action) 

147 

148 # build full usage string 

149 format = self._format_actions_usage 

150 action_usage = format(optionals + positionals, groups) 

151 usage = " ".join([s for s in [prog, action_usage] if s]) 

152 

153 # wrap the usage parts if it's too long 

154 text_width = self._width - self._current_indent 

155 if prefix_len + len(strip_color(usage)) > text_width: 

156 

157 # break usage into wrappable parts 

158 part_regexp = ( 

159 r'\(.*?\)+(?=\s|$)|' 

160 r'\[.*?\]+(?=\s|$)|' 

161 r'\S+' 

162 ) 

163 opt_usage = format(optionals, groups) 

164 pos_usage = format(positionals, groups) 

165 opt_parts = _re.findall(part_regexp, opt_usage) 

166 pos_parts = _re.findall(part_regexp, pos_usage) 

167 assert " ".join(opt_parts) == opt_usage 

168 assert " ".join(pos_parts) == pos_usage 

169 

170 # helper for wrapping lines 

171 def get_lines(parts, indent, prefix=None): 

172 lines = [] 

173 line = [] 

174 indent_length = len(indent) 

175 if prefix is not None: 

176 line_len = prefix_len - 1 

177 else: 

178 line_len = indent_length - 1 

179 for part in parts: 

180 part_len = len(strip_color(part)) 

181 if line_len + 1 + part_len > text_width and line: 

182 lines.append(indent + " ".join(line)) 

183 line = [] 

184 line_len = indent_length - 1 

185 line.append(part) 

186 line_len += part_len + 1 

187 if line: 187 ↛ 189line 187 didn't jump to line 189, because the condition on line 187 was never false

188 lines.append(indent + " ".join(line)) 

189 if prefix is not None: 

190 lines[0] = lines[0][indent_length:] 

191 return lines 

192 

193 # if prog is short, follow it with optionals or positionals 

194 len_prog = len(strip_color(prog)) 

195 if prefix_len + len_prog <= 0.75 * text_width: 

196 indent = " " * (prefix_len + len_prog + 1) 

197 if opt_parts: 197 ↛ 200line 197 didn't jump to line 200, because the condition on line 197 was never false

198 lines = get_lines([prog] + opt_parts, indent, prefix) 

199 lines.extend(get_lines(pos_parts, indent)) 

200 elif pos_parts: 

201 lines = get_lines([prog] + pos_parts, indent, prefix) 

202 else: 

203 lines = [prog] 

204 

205 # if prog is long, put it on its own line 

206 else: 

207 indent = " " * prefix_len 

208 parts = opt_parts + pos_parts 

209 lines = get_lines(parts, indent) 

210 if len(lines) > 1: 210 ↛ 214line 210 didn't jump to line 214, because the condition on line 210 was never false

211 lines = [] 

212 lines.extend(get_lines(opt_parts, indent)) 

213 lines.extend(get_lines(pos_parts, indent)) 

214 lines = [prog] + lines 

215 

216 # join lines into usage 

217 usage = "\n".join(lines) 

218 

219 # prefix with 'usage:' 

220 return "%s%s\n\n" % (prefix, usage) 

221 

222 

223# fmt: on 

224class ColorHelpFormatter(ColorHelpFormatterMixin, HelpFormatter): 

225 pass 

226 

227 

228class ColorTextWrapper(TextWrapper): 

229 # modified upstream code, not going to refactor for complexity. 

230 # fmt: off 

231 def _wrap_chunks(self, chunks): # noqa: C901 

232 """_wrap_chunks(chunks : [string]) -> [string] 

233 

234 Wrap a sequence of text chunks and return a list of lines of 

235 length 'self.width' or less. (If 'break_long_words' is false, 

236 some lines may be longer than this.) Chunks correspond roughly 

237 to words and the whitespace between them: each chunk is 

238 indivisible (modulo 'break_long_words'), but a line break can 

239 come between any two chunks. Chunks should not have internal 

240 whitespace; ie. a chunk is either all whitespace or a "word". 

241 Whitespace chunks will be removed from the beginning and end of 

242 lines, but apart from that whitespace is preserved. 

243 """ 

244 lines = [] 

245 if self.width <= 0: 

246 raise ValueError("invalid width %r (must be > 0)" % self.width) 

247 if self.max_lines is not None: 

248 if self.max_lines > 1: 

249 indent = self.subsequent_indent 

250 else: 

251 indent = self.initial_indent 

252 if len(indent) + len(self.placeholder.lstrip()) > self.width: 

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

254 

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

256 # from a stack of chucks. 

257 chunks.reverse() 

258 

259 while chunks: 

260 

261 # Start the list of chunks that will make up the current line. 

262 # cur_len is just the length of all the chunks in cur_line. 

263 cur_line = [] 

264 cur_len = 0 

265 

266 # Figure out which static string will prefix this line. 

267 if lines: 

268 indent = self.subsequent_indent 

269 else: 

270 indent = self.initial_indent 

271 

272 # Maximum width for this line. 

273 width = self.width - len(indent) 

274 

275 # First chunk on line is whitespace -- drop it, unless this 

276 # is the very beginning of the text (ie. no lines started yet). 

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

278 del chunks[-1] 

279 

280 while chunks: 

281 # modified upstream code, not going to refactor for ambiguous variable name. 

282 l = len(strip_color(chunks[-1])) # noqa: E741 

283 

284 # Can at least squeeze this chunk onto the current line. 

285 # modified upstream code, not going to refactor for ambiguous variable name. 

286 if cur_len + l <= width: # noqa: E741 

287 cur_line.append(chunks.pop()) 

288 cur_len += l 

289 

290 # Nope, this line is full. 

291 else: 

292 break 

293 

294 # The current line is full, and the next chunk is too big to 

295 # fit on *any* line (not just this one). 

296 if chunks and len(strip_color(chunks[-1])) > width: 

297 self._handle_long_word(chunks, cur_line, cur_len, width) 

298 cur_len = sum(map(len, cur_line)) 

299 

300 # If the last chunk on this line is all whitespace, drop it. 

301 if self.drop_whitespace and cur_line and strip_color(cur_line[-1]).strip() == "": 

302 cur_len -= len(strip_color(cur_line[-1])) 

303 del cur_line[-1] 

304 

305 if cur_line: 305 ↛ 259line 305 didn't jump to line 259, because the condition on line 305 was never false

306 if ( 

307 self.max_lines is None 

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

309 or (not chunks or self.drop_whitespace and len(chunks) == 1 and not chunks[0].strip()) 

310 and cur_len <= width 

311 ): 

312 # Convert current line back to a string and store it in 

313 # list of all lines (return value). 

314 lines.append(indent + "".join(cur_line)) 

315 else: 

316 while cur_line: 

317 if strip_color(cur_line[-1]).strip() and cur_len + len(self.placeholder) <= width: 

318 cur_line.append(self.placeholder) 

319 lines.append(indent + "".join(cur_line)) 

320 break 

321 cur_len -= len(strip_color(cur_line[-1])) 

322 del cur_line[-1] 

323 else: 

324 if lines: 

325 prev_line = lines[-1].rstrip() 

326 if len(strip_color(prev_line)) + len(self.placeholder) <= self.width: 326 ↛ 327line 326 didn't jump to line 327, because the condition on line 326 was never true

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

328 break 

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

330 break 

331 

332 return lines 

333 

334 

335# fmt: on 

336 

337 

338class ColorRawDescriptionHelpFormatter(ColorHelpFormatterMixin, RawDescriptionHelpFormatter): 

339 def _fill_text(self, text, width, indent): 

340 return super(RawDescriptionHelpFormatter, self)._fill_text(text, width, indent) 

341 

342 

343class ColorRawTextHelpFormatter(ColorHelpFormatterMixin, RawTextHelpFormatter): 

344 def _split_lines(self, text, width): 

345 return super(RawTextHelpFormatter, self)._split_lines(text, width) 

346 

347 

348class ColorArgumentDefaultsHelpFormatter(ColorHelpFormatterMixin, ArgumentDefaultsHelpFormatter): 

349 pass 

350 

351 

352if "MetavarTypeHelpFormatter" in globals(): 352 ↛ exitline 352 didn't exit the module, because the condition on line 352 was never false

353 

354 class ColorMetavarTypeHelpFormatter(ColorHelpFormatterMixin, MetavarTypeHelpFormatter): 

355 pass