17 from coverage import Coverage |
17 from coverage import Coverage |
18 from coverage import env |
18 from coverage import env |
19 from coverage.collector import CTracer |
19 from coverage.collector import CTracer |
20 from coverage.data import line_counts |
20 from coverage.data import line_counts |
21 from coverage.debug import info_formatter, info_header, short_stack |
21 from coverage.debug import info_formatter, info_header, short_stack |
|
22 from coverage.exceptions import BaseCoverageException, ExceptionDuringRun, NoSource |
22 from coverage.execfile import PyRunner |
23 from coverage.execfile import PyRunner |
23 from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource, output_encoding |
24 from coverage.misc import human_sorted |
24 from coverage.results import should_fail_under |
25 from coverage.results import Numbers, should_fail_under |
25 |
26 |
26 |
27 |
27 class Opts(object): |
28 class Opts: |
28 """A namespace class for individual options we'll build parsers from.""" |
29 """A namespace class for individual options we'll build parsers from.""" |
29 |
30 |
30 append = optparse.make_option( |
31 append = optparse.make_option( |
31 '-a', '--append', action='store_true', |
32 '-a', '--append', action='store_true', |
32 help="Append coverage data to .coverage, otherwise it starts clean each time.", |
33 help="Append coverage data to .coverage, otherwise it starts clean each time.", |
44 ] |
45 ] |
45 concurrency = optparse.make_option( |
46 concurrency = optparse.make_option( |
46 '', '--concurrency', action='store', metavar="LIB", |
47 '', '--concurrency', action='store', metavar="LIB", |
47 choices=CONCURRENCY_CHOICES, |
48 choices=CONCURRENCY_CHOICES, |
48 help=( |
49 help=( |
49 "Properly measure code using a concurrency library. " |
50 "Properly measure code using a concurrency library. " + |
50 "Valid values are: %s." |
51 "Valid values are: {}." |
51 ) % ", ".join(CONCURRENCY_CHOICES), |
52 ).format(", ".join(CONCURRENCY_CHOICES)), |
52 ) |
53 ) |
53 context = optparse.make_option( |
54 context = optparse.make_option( |
54 '', '--context', action='store', metavar="LABEL", |
55 '', '--context', action='store', metavar="LABEL", |
55 help="The context label to record for this coverage run.", |
56 help="The context label to record for this coverage run.", |
|
57 ) |
|
58 contexts = optparse.make_option( |
|
59 '', '--contexts', action='store', |
|
60 metavar="REGEX1,REGEX2,...", |
|
61 help=( |
|
62 "Only display data from lines covered in the given contexts. " + |
|
63 "Accepts Python regexes, which must be quoted." |
|
64 ), |
56 ) |
65 ) |
57 debug = optparse.make_option( |
66 debug = optparse.make_option( |
58 '', '--debug', action='store', metavar="OPTS", |
67 '', '--debug', action='store', metavar="OPTS", |
59 help="Debug options, separated by commas. [env: COVERAGE_DEBUG]", |
68 help="Debug options, separated by commas. [env: COVERAGE_DEBUG]", |
60 ) |
69 ) |
76 ) |
85 ) |
77 include = optparse.make_option( |
86 include = optparse.make_option( |
78 '', '--include', action='store', |
87 '', '--include', action='store', |
79 metavar="PAT1,PAT2,...", |
88 metavar="PAT1,PAT2,...", |
80 help=( |
89 help=( |
81 "Include only files whose paths match one of these patterns. " |
90 "Include only files whose paths match one of these patterns. " + |
82 "Accepts shell-style wildcards, which must be quoted." |
91 "Accepts shell-style wildcards, which must be quoted." |
83 ), |
92 ), |
84 ) |
93 ) |
85 pylib = optparse.make_option( |
94 pylib = optparse.make_option( |
86 '-L', '--pylib', action='store_true', |
95 '-L', '--pylib', action='store_true', |
87 help=( |
96 help=( |
88 "Measure coverage even inside the Python installed library, " |
97 "Measure coverage even inside the Python installed library, " + |
89 "which isn't done by default." |
98 "which isn't done by default." |
90 ), |
99 ), |
91 ) |
|
92 sort = optparse.make_option( |
|
93 '--sort', action='store', metavar='COLUMN', |
|
94 help="Sort the report by the named column: name, stmts, miss, branch, brpart, or cover. " |
|
95 "Default is name." |
|
96 ) |
100 ) |
97 show_missing = optparse.make_option( |
101 show_missing = optparse.make_option( |
98 '-m', '--show-missing', action='store_true', |
102 '-m', '--show-missing', action='store_true', |
99 help="Show line numbers of statements in each module that weren't executed.", |
103 help="Show line numbers of statements in each module that weren't executed.", |
100 ) |
104 ) |
101 skip_covered = optparse.make_option( |
105 module = optparse.make_option( |
102 '--skip-covered', action='store_true', |
106 '-m', '--module', action='store_true', |
103 help="Skip files with 100% coverage.", |
107 help=( |
104 ) |
108 "<pyfile> is an importable Python module, not a script path, " + |
105 no_skip_covered = optparse.make_option( |
109 "to be run as 'python -m' would run it." |
106 '--no-skip-covered', action='store_false', dest='skip_covered', |
110 ), |
107 help="Disable --skip-covered.", |
|
108 ) |
|
109 skip_empty = optparse.make_option( |
|
110 '--skip-empty', action='store_true', |
|
111 help="Skip files with no code.", |
|
112 ) |
|
113 show_contexts = optparse.make_option( |
|
114 '--show-contexts', action='store_true', |
|
115 help="Show contexts for covered lines.", |
|
116 ) |
111 ) |
117 omit = optparse.make_option( |
112 omit = optparse.make_option( |
118 '', '--omit', action='store', |
113 '', '--omit', action='store', |
119 metavar="PAT1,PAT2,...", |
114 metavar="PAT1,PAT2,...", |
120 help=( |
115 help=( |
121 "Omit files whose paths match one of these patterns. " |
116 "Omit files whose paths match one of these patterns. " + |
122 "Accepts shell-style wildcards, which must be quoted." |
117 "Accepts shell-style wildcards, which must be quoted." |
123 ), |
|
124 ) |
|
125 contexts = optparse.make_option( |
|
126 '', '--contexts', action='store', |
|
127 metavar="REGEX1,REGEX2,...", |
|
128 help=( |
|
129 "Only display data from lines covered in the given contexts. " |
|
130 "Accepts Python regexes, which must be quoted." |
|
131 ), |
118 ), |
132 ) |
119 ) |
133 output_xml = optparse.make_option( |
120 output_xml = optparse.make_option( |
134 '-o', '', action='store', dest="outfile", |
121 '-o', '', action='store', dest="outfile", |
135 metavar="OUTFILE", |
122 metavar="OUTFILE", |
145 help="Format the JSON for human readers.", |
132 help="Format the JSON for human readers.", |
146 ) |
133 ) |
147 parallel_mode = optparse.make_option( |
134 parallel_mode = optparse.make_option( |
148 '-p', '--parallel-mode', action='store_true', |
135 '-p', '--parallel-mode', action='store_true', |
149 help=( |
136 help=( |
150 "Append the machine name, process id and random number to the " |
137 "Append the machine name, process id and random number to the " + |
151 ".coverage data file name to simplify collecting data from " |
138 ".coverage data file name to simplify collecting data from " + |
152 "many processes." |
139 "many processes." |
153 ), |
|
154 ) |
|
155 module = optparse.make_option( |
|
156 '-m', '--module', action='store_true', |
|
157 help=( |
|
158 "<pyfile> is an importable Python module, not a script path, " |
|
159 "to be run as 'python -m' would run it." |
|
160 ), |
140 ), |
161 ) |
141 ) |
162 precision = optparse.make_option( |
142 precision = optparse.make_option( |
163 '', '--precision', action='store', metavar='N', type=int, |
143 '', '--precision', action='store', metavar='N', type=int, |
164 help=( |
144 help=( |
165 "Number of digits after the decimal point to display for " |
145 "Number of digits after the decimal point to display for " + |
166 "reported coverage percentages." |
146 "reported coverage percentages." |
167 ), |
147 ), |
|
148 ) |
|
149 quiet = optparse.make_option( |
|
150 '-q', '--quiet', action='store_true', |
|
151 help="Don't print messages about what is happening.", |
168 ) |
152 ) |
169 rcfile = optparse.make_option( |
153 rcfile = optparse.make_option( |
170 '', '--rcfile', action='store', |
154 '', '--rcfile', action='store', |
171 help=( |
155 help=( |
172 "Specify configuration file. " |
156 "Specify configuration file. " + |
173 "By default '.coveragerc', 'setup.cfg', 'tox.ini', and " |
157 "By default '.coveragerc', 'setup.cfg', 'tox.ini', and " + |
174 "'pyproject.toml' are tried. [env: COVERAGE_RCFILE]" |
158 "'pyproject.toml' are tried. [env: COVERAGE_RCFILE]" |
175 ), |
159 ), |
|
160 ) |
|
161 show_contexts = optparse.make_option( |
|
162 '--show-contexts', action='store_true', |
|
163 help="Show contexts for covered lines.", |
|
164 ) |
|
165 skip_covered = optparse.make_option( |
|
166 '--skip-covered', action='store_true', |
|
167 help="Skip files with 100% coverage.", |
|
168 ) |
|
169 no_skip_covered = optparse.make_option( |
|
170 '--no-skip-covered', action='store_false', dest='skip_covered', |
|
171 help="Disable --skip-covered.", |
|
172 ) |
|
173 skip_empty = optparse.make_option( |
|
174 '--skip-empty', action='store_true', |
|
175 help="Skip files with no code.", |
|
176 ) |
|
177 sort = optparse.make_option( |
|
178 '--sort', action='store', metavar='COLUMN', |
|
179 help="Sort the report by the named column: name, stmts, miss, branch, brpart, or cover. " + |
|
180 "Default is name." |
176 ) |
181 ) |
177 source = optparse.make_option( |
182 source = optparse.make_option( |
178 '', '--source', action='store', metavar="SRC1,SRC2,...", |
183 '', '--source', action='store', metavar="SRC1,SRC2,...", |
179 help="A list of packages or directories of code to be measured.", |
184 help="A list of directories or importable names of code to measure.", |
180 ) |
185 ) |
181 timid = optparse.make_option( |
186 timid = optparse.make_option( |
182 '', '--timid', action='store_true', |
187 '', '--timid', action='store_true', |
183 help=( |
188 help=( |
184 "Use a simpler but slower trace method. Try this if you get " |
189 "Use a simpler but slower trace method. Try this if you get " + |
185 "seemingly impossible results!" |
190 "seemingly impossible results!" |
186 ), |
191 ), |
187 ) |
192 ) |
188 title = optparse.make_option( |
193 title = optparse.make_option( |
189 '', '--title', action='store', metavar="TITLE", |
194 '', '--title', action='store', metavar="TITLE", |
193 '', '--version', action='store_true', |
198 '', '--version', action='store_true', |
194 help="Display version information and exit.", |
199 help="Display version information and exit.", |
195 ) |
200 ) |
196 |
201 |
197 |
202 |
198 class CoverageOptionParser(optparse.OptionParser, object): |
203 class CoverageOptionParser(optparse.OptionParser): |
199 """Base OptionParser for coverage.py. |
204 """Base OptionParser for coverage.py. |
200 |
205 |
201 Problems don't exit the program. |
206 Problems don't exit the program. |
202 Defaults are initialized for all options. |
207 Defaults are initialized for all options. |
203 |
208 |
204 """ |
209 """ |
205 |
210 |
206 def __init__(self, *args, **kwargs): |
211 def __init__(self, *args, **kwargs): |
207 super(CoverageOptionParser, self).__init__( |
212 super().__init__( |
208 add_help_option=False, *args, **kwargs |
213 add_help_option=False, *args, **kwargs |
209 ) |
214 ) |
210 self.set_defaults( |
215 self.set_defaults( |
211 action=None, |
216 action=None, |
212 append=None, |
217 append=None, |
213 branch=None, |
218 branch=None, |
214 concurrency=None, |
219 concurrency=None, |
215 context=None, |
220 context=None, |
|
221 contexts=None, |
216 debug=None, |
222 debug=None, |
217 directory=None, |
223 directory=None, |
218 fail_under=None, |
224 fail_under=None, |
219 help=None, |
225 help=None, |
220 ignore_errors=None, |
226 ignore_errors=None, |
221 include=None, |
227 include=None, |
222 keep=None, |
228 keep=None, |
223 module=None, |
229 module=None, |
224 omit=None, |
230 omit=None, |
225 contexts=None, |
|
226 parallel_mode=None, |
231 parallel_mode=None, |
227 precision=None, |
232 precision=None, |
228 pylib=None, |
233 pylib=None, |
|
234 quiet=None, |
229 rcfile=True, |
235 rcfile=True, |
|
236 show_contexts=None, |
230 show_missing=None, |
237 show_missing=None, |
231 skip_covered=None, |
238 skip_covered=None, |
232 skip_empty=None, |
239 skip_empty=None, |
233 show_contexts=None, |
|
234 sort=None, |
240 sort=None, |
235 source=None, |
241 source=None, |
236 timid=None, |
242 timid=None, |
237 title=None, |
243 title=None, |
238 version=None, |
244 version=None, |
287 `description` is the description of the command, for the help text. |
293 `description` is the description of the command, for the help text. |
288 |
294 |
289 """ |
295 """ |
290 if usage: |
296 if usage: |
291 usage = "%prog " + usage |
297 usage = "%prog " + usage |
292 super(CmdOptionParser, self).__init__( |
298 super().__init__( |
293 usage=usage, |
299 usage=usage, |
294 description=description, |
300 description=description, |
295 ) |
301 ) |
296 self.set_defaults(action=action, **(defaults or {})) |
302 self.set_defaults(action=action, **(defaults or {})) |
297 self.add_options(options) |
303 self.add_options(options) |
298 self.cmd = action |
304 self.cmd = action |
299 |
305 |
300 def __eq__(self, other): |
306 def __eq__(self, other): |
301 # A convenience equality, so that I can put strings in unit test |
307 # A convenience equality, so that I can put strings in unit test |
302 # results, and they will compare equal to objects. |
308 # results, and they will compare equal to objects. |
303 return (other == "<CmdOptionParser:%s>" % self.cmd) |
309 return (other == f"<CmdOptionParser:{self.cmd}>") |
304 |
310 |
305 __hash__ = None # This object doesn't need to be hashed. |
311 __hash__ = None # This object doesn't need to be hashed. |
306 |
312 |
307 def get_prog_name(self): |
313 def get_prog_name(self): |
308 """Override of an undocumented function in optparse.OptionParser.""" |
314 """Override of an undocumented function in optparse.OptionParser.""" |
309 program_name = super(CmdOptionParser, self).get_prog_name() |
315 program_name = super().get_prog_name() |
310 |
316 |
311 # Include the sub-command for this parser as part of the command. |
317 # Include the sub-command for this parser as part of the command. |
312 return "{command} {subcommand}".format(command=program_name, subcommand=self.cmd) |
318 return f"{program_name} {self.cmd}" |
313 |
319 |
314 |
320 |
315 GLOBAL_ARGS = [ |
321 GLOBAL_ARGS = [ |
316 Opts.debug, |
322 Opts.debug, |
317 Opts.help, |
323 Opts.help, |
327 Opts.include, |
333 Opts.include, |
328 Opts.omit, |
334 Opts.omit, |
329 ] + GLOBAL_ARGS, |
335 ] + GLOBAL_ARGS, |
330 usage="[options] [modules]", |
336 usage="[options] [modules]", |
331 description=( |
337 description=( |
332 "Make annotated copies of the given files, marking statements that are executed " |
338 "Make annotated copies of the given files, marking statements that are executed " + |
333 "with > and statements that are missed with !." |
339 "with > and statements that are missed with !." |
334 ), |
340 ), |
335 ), |
341 ), |
336 |
342 |
337 'combine': CmdOptionParser( |
343 'combine': CmdOptionParser( |
338 "combine", |
344 "combine", |
339 [ |
345 [ |
340 Opts.append, |
346 Opts.append, |
341 Opts.keep, |
347 Opts.keep, |
|
348 Opts.quiet, |
342 ] + GLOBAL_ARGS, |
349 ] + GLOBAL_ARGS, |
343 usage="[options] <path1> <path2> ... <pathN>", |
350 usage="[options] <path1> <path2> ... <pathN>", |
344 description=( |
351 description=( |
345 "Combine data from multiple coverage files collected " |
352 "Combine data from multiple coverage files collected " + |
346 "with 'run -p'. The combined results are written to a single " |
353 "with 'run -p'. The combined results are written to a single " + |
347 "file representing the union of the data. The positional " |
354 "file representing the union of the data. The positional " + |
348 "arguments are data files or directories containing data files. " |
355 "arguments are data files or directories containing data files. " + |
349 "If no paths are provided, data files in the default data file's " |
356 "If no paths are provided, data files in the default data file's " + |
350 "directory are combined." |
357 "directory are combined." |
351 ), |
358 ), |
352 ), |
359 ), |
353 |
360 |
354 'debug': CmdOptionParser( |
361 'debug': CmdOptionParser( |
355 "debug", GLOBAL_ARGS, |
362 "debug", GLOBAL_ARGS, |
356 usage="<topic>", |
363 usage="<topic>", |
357 description=( |
364 description=( |
358 "Display information about the internals of coverage.py, " |
365 "Display information about the internals of coverage.py, " + |
359 "for diagnosing problems. " |
366 "for diagnosing problems. " + |
360 "Topics are: " |
367 "Topics are: " + |
361 "'data' to show a summary of the collected data; " |
368 "'data' to show a summary of the collected data; " + |
362 "'sys' to show installation information; " |
369 "'sys' to show installation information; " + |
363 "'config' to show the configuration; " |
370 "'config' to show the configuration; " + |
364 "'premain' to show what is calling coverage." |
371 "'premain' to show what is calling coverage." |
365 ), |
372 ), |
366 ), |
373 ), |
367 |
374 |
368 'erase': CmdOptionParser( |
375 'erase': CmdOptionParser( |
496 else: |
506 else: |
497 help_params['extension_modifier'] = 'without C extension' |
507 help_params['extension_modifier'] = 'without C extension' |
498 |
508 |
499 if error: |
509 if error: |
500 print(error, file=sys.stderr) |
510 print(error, file=sys.stderr) |
501 print("Use '%s help' for help." % (program_name,), file=sys.stderr) |
511 print(f"Use '{program_name} help' for help.", file=sys.stderr) |
502 elif parser: |
512 elif parser: |
503 print(parser.format_help().strip()) |
513 print(parser.format_help().strip()) |
504 print() |
514 print() |
505 else: |
515 else: |
506 help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip() |
516 help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip() |
507 if help_msg: |
517 if help_msg: |
508 print(help_msg.format(**help_params)) |
518 print(help_msg.format(**help_params)) |
509 else: |
519 else: |
510 print("Don't know topic %r" % topic) |
520 print(f"Don't know topic {topic!r}") |
511 print("Full documentation is at {__url__}".format(**help_params)) |
521 print("Full documentation is at {__url__}".format(**help_params)) |
512 |
522 |
513 |
523 |
514 OK, ERR, FAIL_UNDER = 0, 1, 2 |
524 OK, ERR, FAIL_UNDER = 0, 1, 2 |
515 |
525 |
516 |
526 |
517 class CoverageScript(object): |
527 class CoverageScript: |
518 """The command-line interface to coverage.py.""" |
528 """The command-line interface to coverage.py.""" |
519 |
529 |
520 def __init__(self): |
530 def __init__(self): |
521 self.global_option = False |
531 self.global_option = False |
522 self.coverage = None |
532 self.coverage = None |
644 outfile=outfile, |
655 outfile=outfile, |
645 pretty_print=options.pretty_print, |
656 pretty_print=options.pretty_print, |
646 show_contexts=options.show_contexts, |
657 show_contexts=options.show_contexts, |
647 **report_args |
658 **report_args |
648 ) |
659 ) |
|
660 else: |
|
661 # There are no other possible actions. |
|
662 raise AssertionError |
649 |
663 |
650 if total is not None: |
664 if total is not None: |
651 # Apply the command line fail-under options, and then use the config |
665 # Apply the command line fail-under options, and then use the config |
652 # value, so we can get fail_under from the config file. |
666 # value, so we can get fail_under from the config file. |
653 if options.fail_under is not None: |
667 if options.fail_under is not None: |
654 self.coverage.set_option("report:fail_under", options.fail_under) |
668 self.coverage.set_option("report:fail_under", options.fail_under) |
655 |
669 |
656 fail_under = self.coverage.get_option("report:fail_under") |
670 fail_under = self.coverage.get_option("report:fail_under") |
657 precision = self.coverage.get_option("report:precision") |
671 precision = self.coverage.get_option("report:precision") |
658 if should_fail_under(total, fail_under, precision): |
672 if should_fail_under(total, fail_under, precision): |
659 msg = "total of {total:.{p}f} is less than fail-under={fail_under:.{p}f}".format( |
673 msg = "total of {total} is less than fail-under={fail_under:.{p}f}".format( |
660 total=total, fail_under=fail_under, p=precision, |
674 total=Numbers(precision=precision).display_covered(total), |
|
675 fail_under=fail_under, |
|
676 p=precision, |
661 ) |
677 ) |
662 print("Coverage failure:", msg) |
678 print("Coverage failure:", msg) |
663 return FAIL_UNDER |
679 return FAIL_UNDER |
664 |
680 |
665 return OK |
681 return OK |
725 for opt_name in ['branch', 'include', 'omit', 'pylib', 'source', 'timid']: |
741 for opt_name in ['branch', 'include', 'omit', 'pylib', 'source', 'timid']: |
726 # As it happens, all of these options have no default, meaning |
742 # As it happens, all of these options have no default, meaning |
727 # they will be None if they have not been specified. |
743 # they will be None if they have not been specified. |
728 if getattr(options, opt_name) is not None: |
744 if getattr(options, opt_name) is not None: |
729 show_help( |
745 show_help( |
730 "Options affecting multiprocessing must only be specified " |
746 "Options affecting multiprocessing must only be specified " + |
731 "in a configuration file.\n" |
747 "in a configuration file.\n" + |
732 "Remove --{} from the command line.".format(opt_name) |
748 f"Remove --{opt_name} from the command line." |
733 ) |
749 ) |
734 return ERR |
750 return ERR |
|
751 |
|
752 os.environ["COVERAGE_RUN"] = "true" |
735 |
753 |
736 runner = PyRunner(args, as_module=bool(options.module)) |
754 runner = PyRunner(args, as_module=bool(options.module)) |
737 runner.prepare() |
755 runner.prepare() |
738 |
756 |
739 if options.append: |
757 if options.append: |
764 for info in args: |
782 for info in args: |
765 if info == 'sys': |
783 if info == 'sys': |
766 sys_info = self.coverage.sys_info() |
784 sys_info = self.coverage.sys_info() |
767 print(info_header("sys")) |
785 print(info_header("sys")) |
768 for line in info_formatter(sys_info): |
786 for line in info_formatter(sys_info): |
769 print(" %s" % line) |
787 print(f" {line}") |
770 elif info == 'data': |
788 elif info == 'data': |
771 self.coverage.load() |
789 self.coverage.load() |
772 data = self.coverage.get_data() |
790 data = self.coverage.get_data() |
773 print(info_header("data")) |
791 print(info_header("data")) |
774 print("path: %s" % data.data_filename()) |
792 print(f"path: {data.data_filename()}") |
775 if data: |
793 if data: |
776 print("has_arcs: %r" % data.has_arcs()) |
794 print(f"has_arcs: {data.has_arcs()!r}") |
777 summary = line_counts(data, fullpath=True) |
795 summary = line_counts(data, fullpath=True) |
778 filenames = sorted(summary.keys()) |
796 filenames = human_sorted(summary.keys()) |
779 print("\n%d files:" % len(filenames)) |
797 print(f"\n{len(filenames)} files:") |
780 for f in filenames: |
798 for f in filenames: |
781 line = "%s: %d lines" % (f, summary[f]) |
799 line = f"{f}: {summary[f]} lines" |
782 plugin = data.file_tracer(f) |
800 plugin = data.file_tracer(f) |
783 if plugin: |
801 if plugin: |
784 line += " [%s]" % plugin |
802 line += f" [{plugin}]" |
785 print(line) |
803 print(line) |
786 else: |
804 else: |
787 print("No data collected") |
805 print("No data collected") |
788 elif info == 'config': |
806 elif info == 'config': |
789 print(info_header("config")) |
807 print(info_header("config")) |
790 config_info = self.coverage.config.__dict__.items() |
808 config_info = self.coverage.config.__dict__.items() |
791 for line in info_formatter(config_info): |
809 for line in info_formatter(config_info): |
792 print(" %s" % line) |
810 print(f" {line}") |
793 elif info == "premain": |
811 elif info == "premain": |
794 print(info_header("premain")) |
812 print(info_header("premain")) |
795 print(short_stack()) |
813 print(short_stack()) |
796 else: |
814 else: |
797 show_help("Don't know what you mean by %r" % info) |
815 show_help(f"Don't know what you mean by {info!r}") |
798 return ERR |
816 return ERR |
799 |
817 |
800 return OK |
818 return OK |
801 |
819 |
802 |
820 |