diff --git a/setup.py b/setup.py index f72ffb9..7aee0f7 100644 --- a/setup.py +++ b/setup.py @@ -104,6 +104,11 @@ def limited_api_args(): install_requires=[ "tabulate" ], + entry_points={ + "console_scripts": [ + "slipcover=slipcover.__main__:main", + ], + }, classifiers=[ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", diff --git a/src/slipcover/__main__.py b/src/slipcover/__main__.py index 11e003f..7da792f 100644 --- a/src/slipcover/__main__.py +++ b/src/slipcover/__main__.py @@ -17,117 +17,125 @@ # showing it what we need. # import argparse -ap = argparse.ArgumentParser(prog='slipcover') -ap.add_argument('--branch', action='store_true', help="measure both branch and line coverage") -ap.add_argument('--json', action='store_true', help="select JSON output") -ap.add_argument('--pretty-print', action='store_true', help="pretty-print JSON output") -ap.add_argument('--out', type=Path, help="specify output file name") -ap.add_argument('--source', help="specify directories to cover") -ap.add_argument('--omit', help="specify file(s) to omit") -ap.add_argument('--immediate', action='store_true', - help=(argparse.SUPPRESS if platform.python_implementation() == "PyPy" else "request immediate de-instrumentation")) -ap.add_argument('--skip-covered', action='store_true', help="omit fully covered files (from text, non-JSON output)") -ap.add_argument('--fail-under', type=float, default=0, help="fail execution with RC 2 if the overall coverage lays lower than this") -ap.add_argument('--threshold', type=int, default=50, metavar="T", - help="threshold for de-instrumentation (if not immediate)") -ap.add_argument('--missing-width', type=int, default=80, metavar="WIDTH", help="maximum width for `missing' column") - -# intended for slipcover development only -ap.add_argument('--silent', action='store_true', help=argparse.SUPPRESS) -ap.add_argument('--dis', action='store_true', help=argparse.SUPPRESS) -ap.add_argument('--debug', action='store_true', help=argparse.SUPPRESS) -ap.add_argument('--dont-wrap-pytest', action='store_true', help=argparse.SUPPRESS) - -g = ap.add_mutually_exclusive_group(required=True) -g.add_argument('-m', dest='module', nargs=1, help="run given module as __main__") -g.add_argument('script', nargs='?', type=Path, help="the script to run") -ap.add_argument('script_or_module_args', nargs=argparse.REMAINDER) - -if '-m' in sys.argv: # work around exclusive group not handled properly - minus_m = sys.argv.index('-m') - args = ap.parse_args(sys.argv[1:minus_m+2]) - args.script_or_module_args = sys.argv[minus_m+2:] -else: - args = ap.parse_args(sys.argv[1:]) - -base_path = Path(args.script).resolve().parent if args.script \ - else Path('.').resolve() - - -file_matcher = sc.FileMatcher() - -if args.source: - for s in args.source.split(','): - file_matcher.addSource(s) -elif args.script: - file_matcher.addSource(Path(args.script).resolve().parent) - -if args.omit: - for o in args.omit.split(','): - file_matcher.addOmit(o) - - -sci = sc.Slipcover(immediate=args.immediate, - d_miss_threshold=args.threshold, branch=args.branch, - skip_covered=args.skip_covered, disassemble=args.dis) - - -if not args.dont_wrap_pytest: - sc.wrap_pytest(sci, file_matcher) - - - -def print_coverage(outfile): - if args.json: - import json - print(json.dumps(sci.get_coverage(), indent=(4 if args.pretty_print else None)), - file=outfile) + +def main(): + ap = argparse.ArgumentParser(prog='slipcover') + ap.add_argument('--branch', action='store_true', help="measure both branch and line coverage") + ap.add_argument('--json', action='store_true', help="select JSON output") + ap.add_argument('--pretty-print', action='store_true', help="pretty-print JSON output") + ap.add_argument('--out', type=Path, help="specify output file name") + ap.add_argument('--source', help="specify directories to cover") + ap.add_argument('--omit', help="specify file(s) to omit") + ap.add_argument('--immediate', action='store_true', + help=(argparse.SUPPRESS if platform.python_implementation() == "PyPy" else "request immediate de-instrumentation")) + ap.add_argument('--skip-covered', action='store_true', help="omit fully covered files (from text, non-JSON output)") + ap.add_argument('--fail-under', type=float, default=0, help="fail execution with RC 2 if the overall coverage lays lower than this") + ap.add_argument('--threshold', type=int, default=50, metavar="T", + help="threshold for de-instrumentation (if not immediate)") + ap.add_argument('--missing-width', type=int, default=80, metavar="WIDTH", help="maximum width for `missing' column") + + # intended for slipcover development only + ap.add_argument('--silent', action='store_true', help=argparse.SUPPRESS) + ap.add_argument('--dis', action='store_true', help=argparse.SUPPRESS) + ap.add_argument('--debug', action='store_true', help=argparse.SUPPRESS) + ap.add_argument('--dont-wrap-pytest', action='store_true', help=argparse.SUPPRESS) + + g = ap.add_mutually_exclusive_group(required=True) + g.add_argument('-m', dest='module', nargs=1, help="run given module as __main__") + g.add_argument('script', nargs='?', type=Path, help="the script to run") + ap.add_argument('script_or_module_args', nargs=argparse.REMAINDER) + + if '-m' in sys.argv: # work around exclusive group not handled properly + minus_m = sys.argv.index('-m') + args = ap.parse_args(sys.argv[1:minus_m+2]) + args.script_or_module_args = sys.argv[minus_m+2:] else: - sci.print_coverage(missing_width=args.missing_width, outfile=outfile) + args = ap.parse_args(sys.argv[1:]) + base_path = Path(args.script).resolve().parent if args.script \ + else Path('.').resolve() -def sci_atexit(): - if args.out: - with open(args.out, "w") as outfile: - print_coverage(outfile) - else: - print_coverage(sys.stdout) -if not args.silent: - atexit.register(sci_atexit) + file_matcher = sc.FileMatcher() + + if args.source: + for s in args.source.split(','): + file_matcher.addSource(s) + elif args.script: + file_matcher.addSource(Path(args.script).resolve().parent) + + if args.omit: + for o in args.omit.split(','): + file_matcher.addOmit(o) + + + sci = sc.Slipcover(immediate=args.immediate, + d_miss_threshold=args.threshold, branch=args.branch, + skip_covered=args.skip_covered, disassemble=args.dis) + + + if not args.dont_wrap_pytest: + sc.wrap_pytest(sci, file_matcher) + -if args.script: - # python 'globals' for the script being executed - script_globals: Dict[Any, Any] = dict() + def print_coverage(outfile): + if args.json: + import json + print(json.dumps(sci.get_coverage(), indent=(4 if args.pretty_print else None)), + file=outfile) + else: + sci.print_coverage(missing_width=args.missing_width, outfile=outfile) - # needed so that the script being invoked behaves like the main one - script_globals['__name__'] = '__main__' - script_globals['__file__'] = args.script - sys.argv = [args.script, *args.script_or_module_args] + def sci_atexit(): + if args.out: + with open(args.out, "w") as outfile: + print_coverage(outfile) + else: + print_coverage(sys.stdout) - # the 1st item in sys.path is always the main script's directory - sys.path.pop(0) - sys.path.insert(0, str(base_path)) + if not args.silent: + atexit.register(sci_atexit) - with open(args.script, "r") as f: - t = ast.parse(f.read()) - if args.branch: - t = br.preinstrument(t) - code = compile(t, str(Path(args.script).resolve()), "exec") - code = sci.instrument(code) - with sc.ImportManager(sci, file_matcher): - exec(code, script_globals) + if args.script: + # python 'globals' for the script being executed + script_globals: Dict[Any, Any] = dict() + + # needed so that the script being invoked behaves like the main one + script_globals['__name__'] = '__main__' + script_globals['__file__'] = args.script + + sys.argv = [args.script, *args.script_or_module_args] + + # the 1st item in sys.path is always the main script's directory + sys.path.pop(0) + sys.path.insert(0, str(base_path)) + + with open(args.script, "r") as f: + t = ast.parse(f.read()) + if args.branch: + t = br.preinstrument(t) + code = compile(t, str(Path(args.script).resolve()), "exec") + + code = sci.instrument(code) + with sc.ImportManager(sci, file_matcher): + exec(code, script_globals) + + else: + import runpy + sys.argv = [*args.module, *args.script_or_module_args] + with sc.ImportManager(sci, file_matcher): + runpy.run_module(*args.module, run_name='__main__', alter_sys=True) + + if args.fail_under: + cov = sci.get_coverage() + if cov['summary']['percent_covered'] < args.fail_under: + return 2 + + return 0 -else: - import runpy - sys.argv = [*args.module, *args.script_or_module_args] - with sc.ImportManager(sci, file_matcher): - runpy.run_module(*args.module, run_name='__main__', alter_sys=True) -if args.fail_under: - cov = sci.get_coverage() - if cov['summary']['percent_covered'] < args.fail_under: - sys.exit(2) +if __name__ == "__main__": + raise SystemExit(main())