1 #!/usr/bin/env python3
2
3 # portions copyright 2001, Autonomous Zones Industries, Inc., all rights...
4 # err... reserved and offered to the public under the terms of the
5 # Python 2.2 license.
6 # Author: Zooko O'Whielacronx
7 # http://zooko.com/
8 # mailto:zooko@zooko.com
9 #
10 # Copyright 2000, Mojam Media, Inc., all rights reserved.
11 # Author: Skip Montanaro
12 #
13 # Copyright 1999, Bioreason, Inc., all rights reserved.
14 # Author: Andrew Dalke
15 #
16 # Copyright 1995-1997, Automatrix, Inc., all rights reserved.
17 # Author: Skip Montanaro
18 #
19 # Copyright 1991-1995, Stichting Mathematisch Centrum, all rights reserved.
20 #
21 #
22 # Permission to use, copy, modify, and distribute this Python software and
23 # its associated documentation for any purpose without fee is hereby
24 # granted, provided that the above copyright notice appears in all copies,
25 # and that both that copyright notice and this permission notice appear in
26 # supporting documentation, and that the name of neither Automatrix,
27 # Bioreason or Mojam Media be used in advertising or publicity pertaining to
28 # distribution of the software without specific, written prior permission.
29 #
30 """program/module to trace Python program or function execution
31
32 Sample use, command line:
33 trace.py -c -f counts --ignore-dir '$prefix' spam.py eggs
34 trace.py -t --ignore-dir '$prefix' spam.py eggs
35 trace.py --trackcalls spam.py eggs
36
37 Sample use, programmatically
38 import sys
39
40 # create a Trace object, telling it what to ignore, and whether to
41 # do tracing or line-counting or both.
42 tracer = trace.Trace(ignoredirs=[sys.base_prefix, sys.base_exec_prefix,],
43 trace=0, count=1)
44 # run the new command using the given tracer
45 tracer.run('main()')
46 # make a report, placing output in /tmp
47 r = tracer.results()
48 r.write_results(show_missing=True, coverdir="/tmp")
49 """
50 __all__ = ['Trace', 'CoverageResults']
51
52 import io
53 import linecache
54 import os
55 import sys
56 import sysconfig
57 import token
58 import tokenize
59 import inspect
60 import gc
61 import dis
62 import pickle
63 from time import monotonic as _time
64
65 import threading
66
67 PRAGMA_NOCOVER = "#pragma NO COVER"
68
69 class ESC[4;38;5;81m_Ignore:
70 def __init__(self, modules=None, dirs=None):
71 self._mods = set() if not modules else set(modules)
72 self._dirs = [] if not dirs else [os.path.normpath(d)
73 for d in dirs]
74 self._ignore = { '<string>': 1 }
75
76 def names(self, filename, modulename):
77 if modulename in self._ignore:
78 return self._ignore[modulename]
79
80 # haven't seen this one before, so see if the module name is
81 # on the ignore list.
82 if modulename in self._mods: # Identical names, so ignore
83 self._ignore[modulename] = 1
84 return 1
85
86 # check if the module is a proper submodule of something on
87 # the ignore list
88 for mod in self._mods:
89 # Need to take some care since ignoring
90 # "cmp" mustn't mean ignoring "cmpcache" but ignoring
91 # "Spam" must also mean ignoring "Spam.Eggs".
92 if modulename.startswith(mod + '.'):
93 self._ignore[modulename] = 1
94 return 1
95
96 # Now check that filename isn't in one of the directories
97 if filename is None:
98 # must be a built-in, so we must ignore
99 self._ignore[modulename] = 1
100 return 1
101
102 # Ignore a file when it contains one of the ignorable paths
103 for d in self._dirs:
104 # The '+ os.sep' is to ensure that d is a parent directory,
105 # as compared to cases like:
106 # d = "/usr/local"
107 # filename = "/usr/local.py"
108 # or
109 # d = "/usr/local.py"
110 # filename = "/usr/local.py"
111 if filename.startswith(d + os.sep):
112 self._ignore[modulename] = 1
113 return 1
114
115 # Tried the different ways, so we don't ignore this module
116 self._ignore[modulename] = 0
117 return 0
118
119 def _modname(path):
120 """Return a plausible module name for the path."""
121
122 base = os.path.basename(path)
123 filename, ext = os.path.splitext(base)
124 return filename
125
126 def _fullmodname(path):
127 """Return a plausible module name for the path."""
128
129 # If the file 'path' is part of a package, then the filename isn't
130 # enough to uniquely identify it. Try to do the right thing by
131 # looking in sys.path for the longest matching prefix. We'll
132 # assume that the rest is the package name.
133
134 comparepath = os.path.normcase(path)
135 longest = ""
136 for dir in sys.path:
137 dir = os.path.normcase(dir)
138 if comparepath.startswith(dir) and comparepath[len(dir)] == os.sep:
139 if len(dir) > len(longest):
140 longest = dir
141
142 if longest:
143 base = path[len(longest) + 1:]
144 else:
145 base = path
146 # the drive letter is never part of the module name
147 drive, base = os.path.splitdrive(base)
148 base = base.replace(os.sep, ".")
149 if os.altsep:
150 base = base.replace(os.altsep, ".")
151 filename, ext = os.path.splitext(base)
152 return filename.lstrip(".")
153
154 class ESC[4;38;5;81mCoverageResults:
155 def __init__(self, counts=None, calledfuncs=None, infile=None,
156 callers=None, outfile=None):
157 self.counts = counts
158 if self.counts is None:
159 self.counts = {}
160 self.counter = self.counts.copy() # map (filename, lineno) to count
161 self.calledfuncs = calledfuncs
162 if self.calledfuncs is None:
163 self.calledfuncs = {}
164 self.calledfuncs = self.calledfuncs.copy()
165 self.callers = callers
166 if self.callers is None:
167 self.callers = {}
168 self.callers = self.callers.copy()
169 self.infile = infile
170 self.outfile = outfile
171 if self.infile:
172 # Try to merge existing counts file.
173 try:
174 with open(self.infile, 'rb') as f:
175 counts, calledfuncs, callers = pickle.load(f)
176 self.update(self.__class__(counts, calledfuncs, callers=callers))
177 except (OSError, EOFError, ValueError) as err:
178 print(("Skipping counts file %r: %s"
179 % (self.infile, err)), file=sys.stderr)
180
181 def is_ignored_filename(self, filename):
182 """Return True if the filename does not refer to a file
183 we want to have reported.
184 """
185 return filename.startswith('<') and filename.endswith('>')
186
187 def update(self, other):
188 """Merge in the data from another CoverageResults"""
189 counts = self.counts
190 calledfuncs = self.calledfuncs
191 callers = self.callers
192 other_counts = other.counts
193 other_calledfuncs = other.calledfuncs
194 other_callers = other.callers
195
196 for key in other_counts:
197 counts[key] = counts.get(key, 0) + other_counts[key]
198
199 for key in other_calledfuncs:
200 calledfuncs[key] = 1
201
202 for key in other_callers:
203 callers[key] = 1
204
205 def write_results(self, show_missing=True, summary=False, coverdir=None):
206 """
207 Write the coverage results.
208
209 :param show_missing: Show lines that had no hits.
210 :param summary: Include coverage summary per module.
211 :param coverdir: If None, the results of each module are placed in its
212 directory, otherwise it is included in the directory
213 specified.
214 """
215 if self.calledfuncs:
216 print()
217 print("functions called:")
218 calls = self.calledfuncs
219 for filename, modulename, funcname in sorted(calls):
220 print(("filename: %s, modulename: %s, funcname: %s"
221 % (filename, modulename, funcname)))
222
223 if self.callers:
224 print()
225 print("calling relationships:")
226 lastfile = lastcfile = ""
227 for ((pfile, pmod, pfunc), (cfile, cmod, cfunc)) \
228 in sorted(self.callers):
229 if pfile != lastfile:
230 print()
231 print("***", pfile, "***")
232 lastfile = pfile
233 lastcfile = ""
234 if cfile != pfile and lastcfile != cfile:
235 print(" -->", cfile)
236 lastcfile = cfile
237 print(" %s.%s -> %s.%s" % (pmod, pfunc, cmod, cfunc))
238
239 # turn the counts data ("(filename, lineno) = count") into something
240 # accessible on a per-file basis
241 per_file = {}
242 for filename, lineno in self.counts:
243 lines_hit = per_file[filename] = per_file.get(filename, {})
244 lines_hit[lineno] = self.counts[(filename, lineno)]
245
246 # accumulate summary info, if needed
247 sums = {}
248
249 for filename, count in per_file.items():
250 if self.is_ignored_filename(filename):
251 continue
252
253 if filename.endswith(".pyc"):
254 filename = filename[:-1]
255
256 if coverdir is None:
257 dir = os.path.dirname(os.path.abspath(filename))
258 modulename = _modname(filename)
259 else:
260 dir = coverdir
261 if not os.path.exists(dir):
262 os.makedirs(dir)
263 modulename = _fullmodname(filename)
264
265 # If desired, get a list of the line numbers which represent
266 # executable content (returned as a dict for better lookup speed)
267 if show_missing:
268 lnotab = _find_executable_linenos(filename)
269 else:
270 lnotab = {}
271 source = linecache.getlines(filename)
272 coverpath = os.path.join(dir, modulename + ".cover")
273 with open(filename, 'rb') as fp:
274 encoding, _ = tokenize.detect_encoding(fp.readline)
275 n_hits, n_lines = self.write_results_file(coverpath, source,
276 lnotab, count, encoding)
277 if summary and n_lines:
278 percent = int(100 * n_hits / n_lines)
279 sums[modulename] = n_lines, percent, modulename, filename
280
281
282 if summary and sums:
283 print("lines cov% module (path)")
284 for m in sorted(sums):
285 n_lines, percent, modulename, filename = sums[m]
286 print("%5d %3d%% %s (%s)" % sums[m])
287
288 if self.outfile:
289 # try and store counts and module info into self.outfile
290 try:
291 with open(self.outfile, 'wb') as f:
292 pickle.dump((self.counts, self.calledfuncs, self.callers),
293 f, 1)
294 except OSError as err:
295 print("Can't save counts files because %s" % err, file=sys.stderr)
296
297 def write_results_file(self, path, lines, lnotab, lines_hit, encoding=None):
298 """Return a coverage results file in path."""
299 # ``lnotab`` is a dict of executable lines, or a line number "table"
300
301 try:
302 outfile = open(path, "w", encoding=encoding)
303 except OSError as err:
304 print(("trace: Could not open %r for writing: %s "
305 "- skipping" % (path, err)), file=sys.stderr)
306 return 0, 0
307
308 n_lines = 0
309 n_hits = 0
310 with outfile:
311 for lineno, line in enumerate(lines, 1):
312 # do the blank/comment match to try to mark more lines
313 # (help the reader find stuff that hasn't been covered)
314 if lineno in lines_hit:
315 outfile.write("%5d: " % lines_hit[lineno])
316 n_hits += 1
317 n_lines += 1
318 elif lineno in lnotab and not PRAGMA_NOCOVER in line:
319 # Highlight never-executed lines, unless the line contains
320 # #pragma: NO COVER
321 outfile.write(">>>>>> ")
322 n_lines += 1
323 else:
324 outfile.write(" ")
325 outfile.write(line.expandtabs(8))
326
327 return n_hits, n_lines
328
329 def _find_lines_from_code(code, strs):
330 """Return dict where keys are lines in the line number table."""
331 linenos = {}
332
333 for _, lineno in dis.findlinestarts(code):
334 if lineno not in strs:
335 linenos[lineno] = 1
336
337 return linenos
338
339 def _find_lines(code, strs):
340 """Return lineno dict for all code objects reachable from code."""
341 # get all of the lineno information from the code of this scope level
342 linenos = _find_lines_from_code(code, strs)
343
344 # and check the constants for references to other code objects
345 for c in code.co_consts:
346 if inspect.iscode(c):
347 # find another code object, so recurse into it
348 linenos.update(_find_lines(c, strs))
349 return linenos
350
351 def _find_strings(filename, encoding=None):
352 """Return a dict of possible docstring positions.
353
354 The dict maps line numbers to strings. There is an entry for
355 line that contains only a string or a part of a triple-quoted
356 string.
357 """
358 d = {}
359 # If the first token is a string, then it's the module docstring.
360 # Add this special case so that the test in the loop passes.
361 prev_ttype = token.INDENT
362 with open(filename, encoding=encoding) as f:
363 tok = tokenize.generate_tokens(f.readline)
364 for ttype, tstr, start, end, line in tok:
365 if ttype == token.STRING:
366 if prev_ttype == token.INDENT:
367 sline, scol = start
368 eline, ecol = end
369 for i in range(sline, eline + 1):
370 d[i] = 1
371 prev_ttype = ttype
372 return d
373
374 def _find_executable_linenos(filename):
375 """Return dict where keys are line numbers in the line number table."""
376 try:
377 with tokenize.open(filename) as f:
378 prog = f.read()
379 encoding = f.encoding
380 except OSError as err:
381 print(("Not printing coverage data for %r: %s"
382 % (filename, err)), file=sys.stderr)
383 return {}
384 code = compile(prog, filename, "exec")
385 strs = _find_strings(filename, encoding)
386 return _find_lines(code, strs)
387
388 class ESC[4;38;5;81mTrace:
389 def __init__(self, count=1, trace=1, countfuncs=0, countcallers=0,
390 ignoremods=(), ignoredirs=(), infile=None, outfile=None,
391 timing=False):
392 """
393 @param count true iff it should count number of times each
394 line is executed
395 @param trace true iff it should print out each line that is
396 being counted
397 @param countfuncs true iff it should just output a list of
398 (filename, modulename, funcname,) for functions
399 that were called at least once; This overrides
400 `count' and `trace'
401 @param ignoremods a list of the names of modules to ignore
402 @param ignoredirs a list of the names of directories to ignore
403 all of the (recursive) contents of
404 @param infile file from which to read stored counts to be
405 added into the results
406 @param outfile file in which to write the results
407 @param timing true iff timing information be displayed
408 """
409 self.infile = infile
410 self.outfile = outfile
411 self.ignore = _Ignore(ignoremods, ignoredirs)
412 self.counts = {} # keys are (filename, linenumber)
413 self.pathtobasename = {} # for memoizing os.path.basename
414 self.donothing = 0
415 self.trace = trace
416 self._calledfuncs = {}
417 self._callers = {}
418 self._caller_cache = {}
419 self.start_time = None
420 if timing:
421 self.start_time = _time()
422 if countcallers:
423 self.globaltrace = self.globaltrace_trackcallers
424 elif countfuncs:
425 self.globaltrace = self.globaltrace_countfuncs
426 elif trace and count:
427 self.globaltrace = self.globaltrace_lt
428 self.localtrace = self.localtrace_trace_and_count
429 elif trace:
430 self.globaltrace = self.globaltrace_lt
431 self.localtrace = self.localtrace_trace
432 elif count:
433 self.globaltrace = self.globaltrace_lt
434 self.localtrace = self.localtrace_count
435 else:
436 # Ahem -- do nothing? Okay.
437 self.donothing = 1
438
439 def run(self, cmd):
440 import __main__
441 dict = __main__.__dict__
442 self.runctx(cmd, dict, dict)
443
444 def runctx(self, cmd, globals=None, locals=None):
445 if globals is None: globals = {}
446 if locals is None: locals = {}
447 if not self.donothing:
448 threading.settrace(self.globaltrace)
449 sys.settrace(self.globaltrace)
450 try:
451 exec(cmd, globals, locals)
452 finally:
453 if not self.donothing:
454 sys.settrace(None)
455 threading.settrace(None)
456
457 def runfunc(self, func, /, *args, **kw):
458 result = None
459 if not self.donothing:
460 sys.settrace(self.globaltrace)
461 try:
462 result = func(*args, **kw)
463 finally:
464 if not self.donothing:
465 sys.settrace(None)
466 return result
467
468 def file_module_function_of(self, frame):
469 code = frame.f_code
470 filename = code.co_filename
471 if filename:
472 modulename = _modname(filename)
473 else:
474 modulename = None
475
476 funcname = code.co_name
477 clsname = None
478 if code in self._caller_cache:
479 if self._caller_cache[code] is not None:
480 clsname = self._caller_cache[code]
481 else:
482 self._caller_cache[code] = None
483 ## use of gc.get_referrers() was suggested by Michael Hudson
484 # all functions which refer to this code object
485 funcs = [f for f in gc.get_referrers(code)
486 if inspect.isfunction(f)]
487 # require len(func) == 1 to avoid ambiguity caused by calls to
488 # new.function(): "In the face of ambiguity, refuse the
489 # temptation to guess."
490 if len(funcs) == 1:
491 dicts = [d for d in gc.get_referrers(funcs[0])
492 if isinstance(d, dict)]
493 if len(dicts) == 1:
494 classes = [c for c in gc.get_referrers(dicts[0])
495 if hasattr(c, "__bases__")]
496 if len(classes) == 1:
497 # ditto for new.classobj()
498 clsname = classes[0].__name__
499 # cache the result - assumption is that new.* is
500 # not called later to disturb this relationship
501 # _caller_cache could be flushed if functions in
502 # the new module get called.
503 self._caller_cache[code] = clsname
504 if clsname is not None:
505 funcname = "%s.%s" % (clsname, funcname)
506
507 return filename, modulename, funcname
508
509 def globaltrace_trackcallers(self, frame, why, arg):
510 """Handler for call events.
511
512 Adds information about who called who to the self._callers dict.
513 """
514 if why == 'call':
515 # XXX Should do a better job of identifying methods
516 this_func = self.file_module_function_of(frame)
517 parent_func = self.file_module_function_of(frame.f_back)
518 self._callers[(parent_func, this_func)] = 1
519
520 def globaltrace_countfuncs(self, frame, why, arg):
521 """Handler for call events.
522
523 Adds (filename, modulename, funcname) to the self._calledfuncs dict.
524 """
525 if why == 'call':
526 this_func = self.file_module_function_of(frame)
527 self._calledfuncs[this_func] = 1
528
529 def globaltrace_lt(self, frame, why, arg):
530 """Handler for call events.
531
532 If the code block being entered is to be ignored, returns `None',
533 else returns self.localtrace.
534 """
535 if why == 'call':
536 code = frame.f_code
537 filename = frame.f_globals.get('__file__', None)
538 if filename:
539 # XXX _modname() doesn't work right for packages, so
540 # the ignore support won't work right for packages
541 modulename = _modname(filename)
542 if modulename is not None:
543 ignore_it = self.ignore.names(filename, modulename)
544 if not ignore_it:
545 if self.trace:
546 print((" --- modulename: %s, funcname: %s"
547 % (modulename, code.co_name)))
548 return self.localtrace
549 else:
550 return None
551
552 def localtrace_trace_and_count(self, frame, why, arg):
553 if why == "line":
554 # record the file name and line number of every trace
555 filename = frame.f_code.co_filename
556 lineno = frame.f_lineno
557 key = filename, lineno
558 self.counts[key] = self.counts.get(key, 0) + 1
559
560 if self.start_time:
561 print('%.2f' % (_time() - self.start_time), end=' ')
562 bname = os.path.basename(filename)
563 print("%s(%d): %s" % (bname, lineno,
564 linecache.getline(filename, lineno)), end='')
565 return self.localtrace
566
567 def localtrace_trace(self, frame, why, arg):
568 if why == "line":
569 # record the file name and line number of every trace
570 filename = frame.f_code.co_filename
571 lineno = frame.f_lineno
572
573 if self.start_time:
574 print('%.2f' % (_time() - self.start_time), end=' ')
575 bname = os.path.basename(filename)
576 print("%s(%d): %s" % (bname, lineno,
577 linecache.getline(filename, lineno)), end='')
578 return self.localtrace
579
580 def localtrace_count(self, frame, why, arg):
581 if why == "line":
582 filename = frame.f_code.co_filename
583 lineno = frame.f_lineno
584 key = filename, lineno
585 self.counts[key] = self.counts.get(key, 0) + 1
586 return self.localtrace
587
588 def results(self):
589 return CoverageResults(self.counts, infile=self.infile,
590 outfile=self.outfile,
591 calledfuncs=self._calledfuncs,
592 callers=self._callers)
593
594 def main():
595 import argparse
596
597 parser = argparse.ArgumentParser()
598 parser.add_argument('--version', action='version', version='trace 2.0')
599
600 grp = parser.add_argument_group('Main options',
601 'One of these (or --report) must be given')
602
603 grp.add_argument('-c', '--count', action='store_true',
604 help='Count the number of times each line is executed and write '
605 'the counts to <module>.cover for each module executed, in '
606 'the module\'s directory. See also --coverdir, --file, '
607 '--no-report below.')
608 grp.add_argument('-t', '--trace', action='store_true',
609 help='Print each line to sys.stdout before it is executed')
610 grp.add_argument('-l', '--listfuncs', action='store_true',
611 help='Keep track of which functions are executed at least once '
612 'and write the results to sys.stdout after the program exits. '
613 'Cannot be specified alongside --trace or --count.')
614 grp.add_argument('-T', '--trackcalls', action='store_true',
615 help='Keep track of caller/called pairs and write the results to '
616 'sys.stdout after the program exits.')
617
618 grp = parser.add_argument_group('Modifiers')
619
620 _grp = grp.add_mutually_exclusive_group()
621 _grp.add_argument('-r', '--report', action='store_true',
622 help='Generate a report from a counts file; does not execute any '
623 'code. --file must specify the results file to read, which '
624 'must have been created in a previous run with --count '
625 '--file=FILE')
626 _grp.add_argument('-R', '--no-report', action='store_true',
627 help='Do not generate the coverage report files. '
628 'Useful if you want to accumulate over several runs.')
629
630 grp.add_argument('-f', '--file',
631 help='File to accumulate counts over several runs')
632 grp.add_argument('-C', '--coverdir',
633 help='Directory where the report files go. The coverage report '
634 'for <package>.<module> will be written to file '
635 '<dir>/<package>/<module>.cover')
636 grp.add_argument('-m', '--missing', action='store_true',
637 help='Annotate executable lines that were not executed with '
638 '">>>>>> "')
639 grp.add_argument('-s', '--summary', action='store_true',
640 help='Write a brief summary for each file to sys.stdout. '
641 'Can only be used with --count or --report')
642 grp.add_argument('-g', '--timing', action='store_true',
643 help='Prefix each line with the time since the program started. '
644 'Only used while tracing')
645
646 grp = parser.add_argument_group('Filters',
647 'Can be specified multiple times')
648 grp.add_argument('--ignore-module', action='append', default=[],
649 help='Ignore the given module(s) and its submodules '
650 '(if it is a package). Accepts comma separated list of '
651 'module names.')
652 grp.add_argument('--ignore-dir', action='append', default=[],
653 help='Ignore files in the given directory '
654 '(multiple directories can be joined by os.pathsep).')
655
656 parser.add_argument('--module', action='store_true', default=False,
657 help='Trace a module. ')
658 parser.add_argument('progname', nargs='?',
659 help='file to run as main program')
660 parser.add_argument('arguments', nargs=argparse.REMAINDER,
661 help='arguments to the program')
662
663 opts = parser.parse_args()
664
665 if opts.ignore_dir:
666 _prefix = sysconfig.get_path("stdlib")
667 _exec_prefix = sysconfig.get_path("platstdlib")
668
669 def parse_ignore_dir(s):
670 s = os.path.expanduser(os.path.expandvars(s))
671 s = s.replace('$prefix', _prefix).replace('$exec_prefix', _exec_prefix)
672 return os.path.normpath(s)
673
674 opts.ignore_module = [mod.strip()
675 for i in opts.ignore_module for mod in i.split(',')]
676 opts.ignore_dir = [parse_ignore_dir(s)
677 for i in opts.ignore_dir for s in i.split(os.pathsep)]
678
679 if opts.report:
680 if not opts.file:
681 parser.error('-r/--report requires -f/--file')
682 results = CoverageResults(infile=opts.file, outfile=opts.file)
683 return results.write_results(opts.missing, opts.summary, opts.coverdir)
684
685 if not any([opts.trace, opts.count, opts.listfuncs, opts.trackcalls]):
686 parser.error('must specify one of --trace, --count, --report, '
687 '--listfuncs, or --trackcalls')
688
689 if opts.listfuncs and (opts.count or opts.trace):
690 parser.error('cannot specify both --listfuncs and (--trace or --count)')
691
692 if opts.summary and not opts.count:
693 parser.error('--summary can only be used with --count or --report')
694
695 if opts.progname is None:
696 parser.error('progname is missing: required with the main options')
697
698 t = Trace(opts.count, opts.trace, countfuncs=opts.listfuncs,
699 countcallers=opts.trackcalls, ignoremods=opts.ignore_module,
700 ignoredirs=opts.ignore_dir, infile=opts.file,
701 outfile=opts.file, timing=opts.timing)
702 try:
703 if opts.module:
704 import runpy
705 module_name = opts.progname
706 mod_name, mod_spec, code = runpy._get_module_details(module_name)
707 sys.argv = [code.co_filename, *opts.arguments]
708 globs = {
709 '__name__': '__main__',
710 '__file__': code.co_filename,
711 '__package__': mod_spec.parent,
712 '__loader__': mod_spec.loader,
713 '__spec__': mod_spec,
714 '__cached__': None,
715 }
716 else:
717 sys.argv = [opts.progname, *opts.arguments]
718 sys.path[0] = os.path.dirname(opts.progname)
719
720 with io.open_code(opts.progname) as fp:
721 code = compile(fp.read(), opts.progname, 'exec')
722 # try to emulate __main__ namespace as much as possible
723 globs = {
724 '__file__': opts.progname,
725 '__name__': '__main__',
726 '__package__': None,
727 '__cached__': None,
728 }
729 t.runctx(code, globs, globs)
730 except OSError as err:
731 sys.exit("Cannot run file %r because: %s" % (sys.argv[0], err))
732 except SystemExit:
733 pass
734
735 results = t.results()
736
737 if not opts.no_report:
738 results.write_results(opts.missing, opts.summary, opts.coverdir)
739
740 if __name__=='__main__':
741 main()