|
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
|
2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt |
|
3 |
|
4 """Reporter foundation for coverage.py.""" |
|
5 |
|
6 import sys |
|
7 |
|
8 from coverage.exceptions import CoverageException, NoDataError, NotPython |
|
9 from coverage.files import prep_patterns, FnmatchMatcher |
|
10 from coverage.misc import ensure_dir_for_file, file_be_gone |
|
11 |
|
12 |
|
13 def render_report(output_path, reporter, morfs, msgfn): |
|
14 """Run a one-file report generator, managing the output file. |
|
15 |
|
16 This function ensures the output file is ready to be written to. Then writes |
|
17 the report to it. Then closes the file and cleans up. |
|
18 |
|
19 """ |
|
20 file_to_close = None |
|
21 delete_file = False |
|
22 |
|
23 if output_path == "-": |
|
24 outfile = sys.stdout |
|
25 else: |
|
26 # Ensure that the output directory is created; done here |
|
27 # because this report pre-opens the output file. |
|
28 # HTMLReport does this using the Report plumbing because |
|
29 # its task is more complex, being multiple files. |
|
30 ensure_dir_for_file(output_path) |
|
31 outfile = open(output_path, "w", encoding="utf-8") |
|
32 file_to_close = outfile |
|
33 |
|
34 try: |
|
35 return reporter.report(morfs, outfile=outfile) |
|
36 except CoverageException: |
|
37 delete_file = True |
|
38 raise |
|
39 finally: |
|
40 if file_to_close: |
|
41 file_to_close.close() |
|
42 if delete_file: |
|
43 file_be_gone(output_path) # pragma: part covered (doesn't return) |
|
44 else: |
|
45 msgfn(f"Wrote {reporter.report_type} to {output_path}") |
|
46 |
|
47 |
|
48 def get_analysis_to_report(coverage, morfs): |
|
49 """Get the files to report on. |
|
50 |
|
51 For each morf in `morfs`, if it should be reported on (based on the omit |
|
52 and include configuration options), yield a pair, the `FileReporter` and |
|
53 `Analysis` for the morf. |
|
54 |
|
55 """ |
|
56 file_reporters = coverage._get_file_reporters(morfs) |
|
57 config = coverage.config |
|
58 |
|
59 if config.report_include: |
|
60 matcher = FnmatchMatcher(prep_patterns(config.report_include), "report_include") |
|
61 file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)] |
|
62 |
|
63 if config.report_omit: |
|
64 matcher = FnmatchMatcher(prep_patterns(config.report_omit), "report_omit") |
|
65 file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)] |
|
66 |
|
67 if not file_reporters: |
|
68 raise NoDataError("No data to report.") |
|
69 |
|
70 for fr in sorted(file_reporters): |
|
71 try: |
|
72 analysis = coverage._analyze(fr) |
|
73 except NotPython: |
|
74 # Only report errors for .py files, and only if we didn't |
|
75 # explicitly suppress those errors. |
|
76 # NotPython is only raised by PythonFileReporter, which has a |
|
77 # should_be_python() method. |
|
78 if fr.should_be_python(): |
|
79 if config.ignore_errors: |
|
80 msg = f"Couldn't parse Python file '{fr.filename}'" |
|
81 coverage._warn(msg, slug="couldnt-parse") |
|
82 else: |
|
83 raise |
|
84 except Exception as exc: |
|
85 if config.ignore_errors: |
|
86 msg = f"Couldn't parse '{fr.filename}': {exc}".rstrip() |
|
87 coverage._warn(msg, slug="couldnt-parse") |
|
88 else: |
|
89 raise |
|
90 else: |
|
91 yield (fr, analysis) |