Coverage for argparse_color_formatter.py: 86%
201 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-18 14:54 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-18 14:54 +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
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.
11__version__ = "2.0.0.post1"
12import re as _re
13from argparse import HelpFormatter
14from argparse import SUPPRESS
15from argparse import ArgumentDefaultsHelpFormatter
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
27from colors import strip_color
30def color_aware_pad(text, width, char=" "):
31 return text + char * (width - len(strip_color(text)))
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)
39 def _split_lines(self, text, width):
40 text = self._whitespace_matcher.sub(" ", text).strip()
41 return ColorTextWrapper(width=width).wrap(text)
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))
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)
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
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)
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
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
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
96 # collect the pieces of the action help
97 parts = [action_header]
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))
108 # or add a newline if the description doesn't end with one
109 elif not action_header.endswith('\n'):
110 parts.append('\n')
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))
116 # return a single string
117 return self._join_parts(parts)
118 # fmt: on
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))
127 # if usage is specified, use that
128 if usage is not None:
129 usage = usage % dict(prog=self._prog)
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)
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)
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)
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])
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:
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
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
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]
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
216 # join lines into usage
217 usage = "\n".join(lines)
219 # prefix with 'usage:'
220 return "%s%s\n\n" % (prefix, usage)
223# fmt: on
224class ColorHelpFormatter(ColorHelpFormatterMixin, HelpFormatter):
225 pass
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]
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")
255 # Arrange in reverse order so items can be efficiently popped
256 # from a stack of chucks.
257 chunks.reverse()
259 while chunks:
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
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
272 # Maximum width for this line.
273 width = self.width - len(indent)
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]
280 while chunks:
281 # modified upstream code, not going to refactor for ambiguous variable name.
282 l = len(strip_color(chunks[-1])) # noqa: E741
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
290 # Nope, this line is full.
291 else:
292 break
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))
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]
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
332 return lines
335# fmt: on
338class ColorRawDescriptionHelpFormatter(ColorHelpFormatterMixin, RawDescriptionHelpFormatter):
339 def _fill_text(self, text, width, indent):
340 return super(RawDescriptionHelpFormatter, self)._fill_text(text, width, indent)
343class ColorRawTextHelpFormatter(ColorHelpFormatterMixin, RawTextHelpFormatter):
344 def _split_lines(self, text, width):
345 return super(RawTextHelpFormatter, self)._split_lines(text, width)
348class ColorArgumentDefaultsHelpFormatter(ColorHelpFormatterMixin, ArgumentDefaultsHelpFormatter):
349 pass
352if "MetavarTypeHelpFormatter" in globals(): 352 ↛ exitline 352 didn't jump to the function exit
354 class ColorMetavarTypeHelpFormatter(ColorHelpFormatterMixin, MetavarTypeHelpFormatter):
355 pass