Coverage for tests.py: 98%
171 statements
« prev ^ index » next coverage.py v7.8.1, created at 2025-05-23 01:23 +0000
« prev ^ index » next coverage.py v7.8.1, created at 2025-05-23 01:23 +0000
1# Copyright (c) 2017, Emergence by Design Inc.
3import argparse
4import os
5import sys
6from collections import OrderedDict
7from functools import partial
8from io import StringIO
9from unittest import TestCase
11from colors import bold
12from colors import color
13from colors import underline
15from argparse_color_formatter import ColorHelpFormatter
16from argparse_color_formatter import ColorTextWrapper
19try:
20 from contextlib import redirect_stderr
21 from contextlib import redirect_stdout
22except ImportError:
23 import contextlib
25 @contextlib.contextmanager
26 def redirect_stdout(target):
27 original = sys.stdout
28 sys.stdout = target
29 yield
30 sys.stdout = original
32 @contextlib.contextmanager
33 def redirect_stderr(target):
34 original = sys.stderr
35 sys.stderr = target
36 yield
37 sys.stderr = original
40colors = OrderedDict(
41 (
42 ("red", partial(color, fg="red", style="bold")),
43 ("orange", partial(color, fg="orange", style="bold")),
44 ("yellow", partial(color, fg="yellow", style="bold")),
45 ("green", partial(color, fg="green", style="bold")),
46 ("blue", partial(color, fg="blue", style="bold")),
47 ("indigo", partial(color, fg="indigo", style="bold")),
48 ("violet", partial(color, fg="violet", style="bold")),
49 )
50)
51positions = ["first", "second", "third", "forth", "fifth", "sixth", "seventh"]
53color_kwargs = {
54 "color": bold("color"),
55 "typically": underline("typically"),
56}
57color_pos = OrderedDict((p, v(p)) for v, p in zip(colors.values(), positions))
58color_names = OrderedDict((k, v(k)) for k, v in colors.items())
60if sys.version_info >= (3, 10): 60 ↛ 67line 60 didn't jump to line 67 because the condition on line 60 was always true
61 color_kwargs.update(
62 {
63 "options_string": "options",
64 }
65 )
66else:
67 color_kwargs.update(
68 {
69 "options_string": "optional arguments",
70 }
71 )
74def rainbow_text(text):
75 retval = []
76 colors_iter = iter(colors.values())
77 cur_color = next(colors_iter)
78 for char in text:
79 retval.append(cur_color(char))
80 try:
81 cur_color = next(colors_iter)
82 except StopIteration:
83 colors_iter = iter(colors.values())
84 cur_color = next(colors_iter)
85 return "".join(retval)
88color_kwargs.update(color_pos)
89color_kwargs.update(color_names)
90color_kwargs.update(
91 {
92 "colorful": rainbow_text("colorful"),
93 "rainbow_maker": rainbow_text("rainbow_maker"),
94 "bow": rainbow_text("bow"),
95 "red-orange-yellow-green-blue-indigo-violet": rainbow_text("red-orange-yellow-green-blue-indigo-violet"),
96 }
97)
100def rainbow_maker_arg_help(color_name):
101 return "{color} used when making rainbow, {typically} this would be {color_name}.".format(
102 color=bold("color"), typically=underline("typically"), color_name=color_kwargs[color_name]
103 )
106def rainbow_maker(args):
107 parser = argparse.ArgumentParser(
108 prog="{rainbow_maker}".format(**color_kwargs),
109 usage="%(prog)s [-h] {first} {second} {third} {forth} {fifth} {sixth} {seventh}".format(**color_kwargs),
110 epilog="This epilog has some {colorful} escapes in it as well and should not wrap on 80.".format(
111 **color_kwargs
112 ),
113 description="This script is a test for {rainbow_maker}. This description consists of 140 chars."
114 " It should be able to fit onto two 80 char lines.".format(**color_kwargs),
115 formatter_class=ColorHelpFormatter,
116 add_help=False,
117 )
118 for arg_name, color_name in zip(color_pos.keys(), color_names.keys()):
119 parser.add_argument(arg_name, default=color_name, help=rainbow_maker_arg_help(color_name))
120 parser.add_argument("-h", "--help", action="help", help="displays this {colorful} help text".format(**color_kwargs))
121 parser.parse_args(args)
124def rainbow_maker_colored_metavar(args, *, longer_help=1):
125 parser = argparse.ArgumentParser(
126 prog="{rainbow_maker}".format(**color_kwargs),
127 # with recent fixes, these will be colored as well if the metavar is colored, and wrapped properly.
128 # so we don't need to do this ourselves anymore.
129 # usage="%(prog)s [-h] {first} {second} {third} {forth} {fifth} {sixth} {seventh}".format(**color_kwargs),
130 epilog="This epilog has some {colorful} escapes in it as well and should not wrap on 80.".format(
131 **color_kwargs
132 ),
133 description="This script is a test for {rainbow_maker}. This description consists of 140 chars."
134 " It should be able to fit onto two 80 char lines.".format(**color_kwargs),
135 formatter_class=ColorHelpFormatter,
136 add_help=False,
137 )
138 for arg_name, color_name in zip(color_pos.keys(), color_names.keys()):
139 parser.add_argument(
140 color_name,
141 metavar=color_kwargs[arg_name],
142 default=color_name,
143 help=rainbow_maker_arg_help(color_name) * longer_help,
144 )
145 parser.add_argument("-h", "--help", action="help", help="displays this {colorful} help text".format(**color_kwargs))
146 parser.parse_args(args)
149def rainbow_maker_auto_usage(args):
150 parser = argparse.ArgumentParser(
151 prog="{rainbow_maker}".format(**color_kwargs),
152 epilog="This epilog has some {colorful} escapes in it as well and should not wrap on 80.".format(
153 **color_kwargs
154 ),
155 description="This script is a test for {rainbow_maker}. This description consists of 140 chars."
156 " It should be able to fit onto two 80 char lines.".format(**color_kwargs),
157 formatter_class=ColorHelpFormatter,
158 add_help=False,
159 )
160 for arg_name, color_name in zip(color_pos.keys(), color_names.keys()):
161 parser.add_argument(arg_name, default=color_name, help=rainbow_maker_arg_help(color_name))
162 parser.add_argument("-h", "--help", action="help", help="displays this {colorful} help text".format(**color_kwargs))
163 parser.parse_args(args)
166def rainbow_maker_auto_usage_short_prog(args):
167 parser = argparse.ArgumentParser(
168 prog="{bow}".format(**color_kwargs),
169 epilog="This epilog has some {colorful} escapes in it as well and should not wrap on 80.".format(
170 **color_kwargs
171 ),
172 description="This script is a test for {rainbow_maker}. This description consists of 140 chars."
173 " It should be able to fit onto two 80 char lines.".format(**color_kwargs),
174 formatter_class=ColorHelpFormatter,
175 add_help=False,
176 )
177 for arg_name, color_name in zip(color_pos.keys(), color_names.keys()):
178 parser.add_argument(arg_name, default=color_name, help=rainbow_maker_arg_help(color_name))
179 parser.add_argument("-h", "--help", action="help", help="displays this {colorful} help text".format(**color_kwargs))
180 parser.parse_args(args)
183def rainbow_maker_auto_usage_long_prog(args):
184 parser = argparse.ArgumentParser(
185 prog="{red-orange-yellow-green-blue-indigo-violet}".format(**color_kwargs),
186 epilog="This epilog has some {colorful} escapes in it as well and should not wrap on 80.".format(
187 **color_kwargs
188 ),
189 description="This script is a test for {rainbow_maker}. This description consists of 140 chars."
190 " It should be able to fit onto two 80 char lines.".format(**color_kwargs),
191 formatter_class=ColorHelpFormatter,
192 add_help=False,
193 )
194 for arg_name, color_name in zip(color_pos.keys(), color_names.keys()):
195 parser.add_argument(arg_name, default=color_name, help=rainbow_maker_arg_help(color_name))
196 parser.add_argument("-h", "--help", action="help", help="displays this {colorful} help text".format(**color_kwargs))
197 parser.parse_args(args)
200def rainbow_maker_no_args(args):
201 parser = argparse.ArgumentParser(
202 prog="{rainbow_maker}".format(**color_kwargs),
203 epilog="This epilog has some {colorful} escapes in it as well and should not wrap on 80.".format(
204 **color_kwargs
205 ),
206 description="This script is a test for {rainbow_maker}. This description consists of 140 chars."
207 " It should be able to fit onto two 80 char lines.".format(**color_kwargs),
208 formatter_class=ColorHelpFormatter,
209 add_help=False,
210 )
211 parser.parse_args(args)
214class TestColorArgsParserOutput(TestCase):
215 maxDiff = None
217 def test_color_output_wrapped_as_expected(self):
218 try:
219 os.environ["COLUMNS"] = "80"
220 out = StringIO()
221 with redirect_stdout(out):
222 self.assertRaises(SystemExit, rainbow_maker, ["-h"])
223 out.seek(0)
224 self.assertEqual(
225 out.read(),
226 "usage: {rainbow_maker} [-h] {first} {second} {third} {forth} {fifth} {sixth} {seventh}\n"
227 "\n"
228 "This script is a test for {rainbow_maker}. This description consists of 140\n"
229 "chars. It should be able to fit onto two 80 char lines.\n"
230 "\n"
231 "positional arguments:\n"
232 " first {color} used when making rainbow, {typically} this would be {red}.\n"
233 " second {color} used when making rainbow, {typically} this would be {orange}.\n"
234 " third {color} used when making rainbow, {typically} this would be {yellow}.\n"
235 " forth {color} used when making rainbow, {typically} this would be {green}.\n"
236 " fifth {color} used when making rainbow, {typically} this would be {blue}.\n"
237 " sixth {color} used when making rainbow, {typically} this would be {indigo}.\n"
238 " seventh {color} used when making rainbow, {typically} this would be {violet}.\n"
239 "\n"
240 "{options_string}:\n"
241 " -h, --help displays this {colorful} help text\n"
242 "\n"
243 "This epilog has some {colorful} escapes in it as well and should not wrap on 80.\n".format(
244 **color_kwargs
245 ),
246 )
247 finally:
248 del os.environ["COLUMNS"]
250 def test_color_output_wrapped_as_expected_small_width(self):
251 try:
252 os.environ["COLUMNS"] = "42"
253 out = StringIO()
254 with redirect_stdout(out):
255 self.assertRaises(SystemExit, rainbow_maker, ["-h"])
256 out.seek(0)
257 self.assertEqual(
258 out.read(),
259 # usage doesnt wrap for some reason when manually specified.
260 # seems like a bug but leaving alone because seems out of scope re: colors.
261 "usage: {rainbow_maker} [-h] {first} {second} {third} {forth} {fifth} {sixth} {seventh}\n"
262 "\n"
263 "This script is a test for {rainbow_maker}.\n"
264 "This description consists of 140 chars.\n"
265 "It should be able to fit onto two 80\n"
266 "char lines.\n"
267 "\n"
268 "positional arguments:\n"
269 " first {color} used when making\n"
270 " rainbow, {typically} this\n"
271 " would be {red}.\n"
272 " second {color} used when making\n"
273 " rainbow, {typically} this\n"
274 " would be {orange}.\n"
275 " third {color} used when making\n"
276 " rainbow, {typically} this\n"
277 " would be {yellow}.\n"
278 " forth {color} used when making\n"
279 " rainbow, {typically} this\n"
280 " would be {green}.\n"
281 " fifth {color} used when making\n"
282 " rainbow, {typically} this\n"
283 " would be {blue}.\n"
284 " sixth {color} used when making\n"
285 " rainbow, {typically} this\n"
286 " would be {indigo}.\n"
287 " seventh {color} used when making\n"
288 " rainbow, {typically} this\n"
289 " would be {violet}.\n"
290 "\n"
291 "{options_string}:\n"
292 " -h, --help displays this {colorful}\n"
293 " help text\n"
294 "\n"
295 "This epilog has some {colorful} escapes in\n"
296 "it as well and should not wrap on 80.\n".format(**color_kwargs),
297 )
298 finally:
299 del os.environ["COLUMNS"]
301 def test_color_output_wrapped_as_expected_with_auto_usage(self):
302 try:
303 os.environ["COLUMNS"] = "80"
304 out = StringIO()
305 with redirect_stdout(out):
306 self.assertRaises(SystemExit, rainbow_maker_auto_usage, ["-h"])
307 out.seek(0)
308 self.assertEqual(
309 out.read(),
310 "usage: {rainbow_maker} [-h] first second third forth fifth sixth seventh\n"
311 "\n"
312 "This script is a test for {rainbow_maker}. This description consists of 140\n"
313 "chars. It should be able to fit onto two 80 char lines.\n"
314 "\n"
315 "positional arguments:\n"
316 " first {color} used when making rainbow, {typically} this would be {red}.\n"
317 " second {color} used when making rainbow, {typically} this would be {orange}.\n"
318 " third {color} used when making rainbow, {typically} this would be {yellow}.\n"
319 " forth {color} used when making rainbow, {typically} this would be {green}.\n"
320 " fifth {color} used when making rainbow, {typically} this would be {blue}.\n"
321 " sixth {color} used when making rainbow, {typically} this would be {indigo}.\n"
322 " seventh {color} used when making rainbow, {typically} this would be {violet}.\n"
323 "\n"
324 "{options_string}:\n"
325 " -h, --help displays this {colorful} help text\n"
326 "\n"
327 "This epilog has some {colorful} escapes in it as well and should not wrap on 80.\n".format(
328 **color_kwargs
329 ),
330 )
331 finally:
332 del os.environ["COLUMNS"]
334 def test_color_output_wrapped_as_expected_with_auto_usage_small_width(self):
335 try:
336 os.environ["COLUMNS"] = "42"
337 out = StringIO()
338 with redirect_stdout(out):
339 self.assertRaises(SystemExit, rainbow_maker_auto_usage, ["-h"])
340 out.seek(0)
341 self.assertEqual(
342 out.read(),
343 "usage: {rainbow_maker} [-h]\n"
344 " first second third\n"
345 " forth fifth sixth\n"
346 " seventh\n"
347 "\n"
348 "This script is a test for {rainbow_maker}.\n"
349 "This description consists of 140 chars.\n"
350 "It should be able to fit onto two 80\n"
351 "char lines.\n"
352 "\n"
353 "positional arguments:\n"
354 " first {color} used when making\n"
355 " rainbow, {typically} this\n"
356 " would be {red}.\n"
357 " second {color} used when making\n"
358 " rainbow, {typically} this\n"
359 " would be {orange}.\n"
360 " third {color} used when making\n"
361 " rainbow, {typically} this\n"
362 " would be {yellow}.\n"
363 " forth {color} used when making\n"
364 " rainbow, {typically} this\n"
365 " would be {green}.\n"
366 " fifth {color} used when making\n"
367 " rainbow, {typically} this\n"
368 " would be {blue}.\n"
369 " sixth {color} used when making\n"
370 " rainbow, {typically} this\n"
371 " would be {indigo}.\n"
372 " seventh {color} used when making\n"
373 " rainbow, {typically} this\n"
374 " would be {violet}.\n"
375 "\n"
376 "{options_string}:\n"
377 " -h, --help displays this {colorful}\n"
378 " help text\n"
379 "\n"
380 "This epilog has some {colorful} escapes in\n"
381 "it as well and should not wrap on 80.\n".format(**color_kwargs),
382 )
383 finally:
384 del os.environ["COLUMNS"]
386 def test_color_output_wrapped_as_expected_with_auto_usage_short_prog_small_width(self):
387 try:
388 os.environ["COLUMNS"] = "42"
389 out = StringIO()
390 with redirect_stdout(out):
391 self.assertRaises(SystemExit, rainbow_maker_auto_usage_short_prog, ["-h"])
392 out.seek(0)
393 self.assertEqual(
394 out.read(),
395 "usage: {bow} [-h]\n"
396 " first second third forth\n"
397 " fifth sixth seventh\n"
398 "\n"
399 "This script is a test for {rainbow_maker}.\n"
400 "This description consists of 140 chars.\n"
401 "It should be able to fit onto two 80\n"
402 "char lines.\n"
403 "\n"
404 "positional arguments:\n"
405 " first {color} used when making\n"
406 " rainbow, {typically} this\n"
407 " would be {red}.\n"
408 " second {color} used when making\n"
409 " rainbow, {typically} this\n"
410 " would be {orange}.\n"
411 " third {color} used when making\n"
412 " rainbow, {typically} this\n"
413 " would be {yellow}.\n"
414 " forth {color} used when making\n"
415 " rainbow, {typically} this\n"
416 " would be {green}.\n"
417 " fifth {color} used when making\n"
418 " rainbow, {typically} this\n"
419 " would be {blue}.\n"
420 " sixth {color} used when making\n"
421 " rainbow, {typically} this\n"
422 " would be {indigo}.\n"
423 " seventh {color} used when making\n"
424 " rainbow, {typically} this\n"
425 " would be {violet}.\n"
426 "\n"
427 "{options_string}:\n"
428 " -h, --help displays this {colorful}\n"
429 " help text\n"
430 "\n"
431 "This epilog has some {colorful} escapes in\n"
432 "it as well and should not wrap on 80.\n".format(**color_kwargs),
433 )
434 finally:
435 del os.environ["COLUMNS"]
437 def test_color_output_wrapped_as_expected_with_auto_usage_long_prog_small_width(self):
438 try:
439 os.environ["COLUMNS"] = "42"
440 out = StringIO()
441 with redirect_stdout(out):
442 self.assertRaises(SystemExit, rainbow_maker_auto_usage_long_prog, ["-h"])
443 out.seek(0)
444 self.assertEqual(
445 out.read(),
446 "usage: {red-orange-yellow-green-blue-indigo-violet}\n"
447 " [-h]\n"
448 " first second third forth fifth\n"
449 " sixth seventh\n"
450 "\n"
451 "This script is a test for {rainbow_maker}.\n"
452 "This description consists of 140 chars.\n"
453 "It should be able to fit onto two 80\n"
454 "char lines.\n"
455 "\n"
456 "positional arguments:\n"
457 " first {color} used when making\n"
458 " rainbow, {typically} this\n"
459 " would be {red}.\n"
460 " second {color} used when making\n"
461 " rainbow, {typically} this\n"
462 " would be {orange}.\n"
463 " third {color} used when making\n"
464 " rainbow, {typically} this\n"
465 " would be {yellow}.\n"
466 " forth {color} used when making\n"
467 " rainbow, {typically} this\n"
468 " would be {green}.\n"
469 " fifth {color} used when making\n"
470 " rainbow, {typically} this\n"
471 " would be {blue}.\n"
472 " sixth {color} used when making\n"
473 " rainbow, {typically} this\n"
474 " would be {indigo}.\n"
475 " seventh {color} used when making\n"
476 " rainbow, {typically} this\n"
477 " would be {violet}.\n"
478 "\n"
479 "{options_string}:\n"
480 " -h, --help displays this {colorful}\n"
481 " help text\n"
482 "\n"
483 "This epilog has some {colorful} escapes in\n"
484 "it as well and should not wrap on 80.\n".format(**color_kwargs),
485 )
486 finally:
487 del os.environ["COLUMNS"]
489 def test_color_output_wrapped_as_expected_with_no_args(self):
490 out = StringIO()
491 with redirect_stderr(out):
492 self.assertRaises(SystemExit, rainbow_maker_no_args, ["--bad"])
493 out.seek(0)
494 self.assertEqual(
495 out.read(),
496 "usage: {rainbow_maker}\n" "{rainbow_maker}: error: unrecognized arguments: --bad\n".format(**color_kwargs),
497 )
499 def test_color_output_with_long_help(self):
500 try:
501 os.environ["COLUMNS"] = "42"
502 out = StringIO()
503 with redirect_stdout(out):
504 self.assertRaises(SystemExit, partial(rainbow_maker_colored_metavar, longer_help=2), ["-h"])
505 out.seek(0)
506 self.assertEqual(
507 out.read(),
508 "usage: {rainbow_maker} [-h]\n"
509 " {first} {second} {third}\n"
510 " {forth} {fifth} {sixth}\n"
511 " {seventh}\n"
512 "\n"
513 "This script is a test for {rainbow_maker}.\n"
514 "This description consists of 140 chars.\n"
515 "It should be able to fit onto two 80\n"
516 "char lines.\n"
517 "\n"
518 "positional arguments:\n"
519 " {first} {color} used when making\n"
520 " rainbow, {typically} this\n"
521 " would be {red}.{color} used\n"
522 " when making rainbow,\n"
523 " {typically} this would be\n"
524 " {red}.\n"
525 " {second} {color} used when making\n"
526 " rainbow, {typically} this\n"
527 " would be {orange}.{color} used\n"
528 " when making rainbow,\n"
529 " {typically} this would be\n"
530 " {orange}.\n"
531 " {third} {color} used when making\n"
532 " rainbow, {typically} this\n"
533 " would be {yellow}.{color} used\n"
534 " when making rainbow,\n"
535 " {typically} this would be\n"
536 " {yellow}.\n"
537 " {forth} {color} used when making\n"
538 " rainbow, {typically} this\n"
539 " would be {green}.{color} used\n"
540 " when making rainbow,\n"
541 " {typically} this would be\n"
542 " {green}.\n"
543 " {fifth} {color} used when making\n"
544 " rainbow, {typically} this\n"
545 " would be {blue}.{color} used\n"
546 " when making rainbow,\n"
547 " {typically} this would be\n"
548 " {blue}.\n"
549 " {sixth} {color} used when making\n"
550 " rainbow, {typically} this\n"
551 " would be {indigo}.{color} used\n"
552 " when making rainbow,\n"
553 " {typically} this would be\n"
554 " {indigo}.\n"
555 " {seventh} {color} used when making\n"
556 " rainbow, {typically} this\n"
557 " would be {violet}.{color} used\n"
558 " when making rainbow,\n"
559 " {typically} this would be\n"
560 " {violet}.\n"
561 "\n"
562 "{options_string}:\n"
563 " -h, --help displays this {colorful}\n"
564 " help text\n"
565 "\n"
566 "This epilog has some {colorful} escapes in\n"
567 "it as well and should not wrap on 80.\n".format(**color_kwargs),
568 )
569 finally:
570 del os.environ["COLUMNS"]
573class TestColorTextWrapper(TestCase):
574 def test_bad_width_error(self):
575 ctw = ColorTextWrapper(width=-1)
576 self.assertRaisesRegex(
577 ValueError, r"invalid width -1 \(must be > 0\)", lambda: ctw.wrap("This is some text to wrap.")
578 )
580 def test_starting_whitespace(self):
581 ctw = ColorTextWrapper(width=20)
582 self.assertEqual(
583 ctw.wrap(" 01234 56789 01234 56789 01234 56789 01234 56789"),
584 [" 01234 56789 01234", "56789 01234 56789", "01234 56789"],
585 )
587 def test_max_lines_and_placeholder(self):
588 ctw = ColorTextWrapper(width=10, max_lines=2, placeholder="**" * 10)
589 self.assertRaisesRegex(
590 ValueError,
591 r"placeholder too large for max width",
592 lambda: ctw.wrap("01234 56789 01234 56789 01234 56789 01234 56789"),
593 )
595 def test_max_lines_and_indent(self):
596 ctw = ColorTextWrapper(width=20, max_lines=2, initial_indent=" ")
597 self.assertEqual(
598 ctw.wrap("01234 56789 01234 56789 01234 56789 01234 56789"), [" 01234 56789 01234", "56789 01234 [...]"]
599 )
601 def test_max_lines_and_subsequence_indent(self):
602 ctw = ColorTextWrapper(width=20, max_lines=0, initial_indent=" ", subsequent_indent=" ")
603 self.assertEqual(ctw.wrap("01234 56789 01234 56789 01234 56789 01234 56789"), [" 01234 56789 [...]"])
605 def test_too_big(self):
606 ctw = ColorTextWrapper(width=10)
607 self.assertEqual(
608 ctw.wrap("0123456789 0123456789 01234567890123456789"),
609 ["0123456789", "0123456789", "0123456789", "0123456789"],
610 )
612 def test_placeholder_edge_case(self):
613 ctw = ColorTextWrapper(width=4, max_lines=1, placeholder="***")
614 self.assertEqual(ctw.wrap("0123456789"), ["***"])
616 def test_placeholder_edge_case_2(self):
617 ctw = ColorTextWrapper(width=5, max_lines=2, placeholder="****")
618 self.assertEqual(ctw.wrap("0123456789 " * 2), ["01234", "****"])
621if __name__ == "__main__": 621 ↛ 622line 621 didn't jump to line 622 because the condition on line 621 was never true
622 rainbow_maker_colored_metavar(None, longer_help=2)