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