|
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
|
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt |
|
3 |
|
4 """Python source expertise for coverage.py""" |
|
5 |
|
6 import os.path |
|
7 import types |
|
8 import zipimport |
|
9 |
|
10 from coverage import env, files |
|
11 from coverage.misc import ( |
|
12 contract, CoverageException, expensive, NoSource, join_regex, isolate_module, |
|
13 ) |
|
14 from coverage.parser import PythonParser |
|
15 from coverage.phystokens import source_token_lines, source_encoding |
|
16 from coverage.plugin import FileReporter |
|
17 |
|
18 os = isolate_module(os) |
|
19 |
|
20 |
|
21 @contract(returns='bytes') |
|
22 def read_python_source(filename): |
|
23 """Read the Python source text from `filename`. |
|
24 |
|
25 Returns bytes. |
|
26 |
|
27 """ |
|
28 with open(filename, "rb") as f: |
|
29 return f.read().replace(b"\r\n", b"\n").replace(b"\r", b"\n") |
|
30 |
|
31 |
|
32 @contract(returns='unicode') |
|
33 def get_python_source(filename): |
|
34 """Return the source code, as unicode.""" |
|
35 base, ext = os.path.splitext(filename) |
|
36 if ext == ".py" and env.WINDOWS: |
|
37 exts = [".py", ".pyw"] |
|
38 else: |
|
39 exts = [ext] |
|
40 |
|
41 for ext in exts: |
|
42 try_filename = base + ext |
|
43 if os.path.exists(try_filename): |
|
44 # A regular text file: open it. |
|
45 source = read_python_source(try_filename) |
|
46 break |
|
47 |
|
48 # Maybe it's in a zip file? |
|
49 source = get_zip_bytes(try_filename) |
|
50 if source is not None: |
|
51 break |
|
52 else: |
|
53 # Couldn't find source. |
|
54 raise NoSource("No source for code: '%s'." % filename) |
|
55 |
|
56 # Replace \f because of http://bugs.python.org/issue19035 |
|
57 source = source.replace(b'\f', b' ') |
|
58 source = source.decode(source_encoding(source), "replace") |
|
59 |
|
60 # Python code should always end with a line with a newline. |
|
61 if source and source[-1] != '\n': |
|
62 source += '\n' |
|
63 |
|
64 return source |
|
65 |
|
66 |
|
67 @contract(returns='bytes|None') |
|
68 def get_zip_bytes(filename): |
|
69 """Get data from `filename` if it is a zip file path. |
|
70 |
|
71 Returns the bytestring data read from the zip file, or None if no zip file |
|
72 could be found or `filename` isn't in it. The data returned will be |
|
73 an empty string if the file is empty. |
|
74 |
|
75 """ |
|
76 markers = ['.zip'+os.sep, '.egg'+os.sep] |
|
77 for marker in markers: |
|
78 if marker in filename: |
|
79 parts = filename.split(marker) |
|
80 try: |
|
81 zi = zipimport.zipimporter(parts[0]+marker[:-1]) |
|
82 except zipimport.ZipImportError: |
|
83 continue |
|
84 try: |
|
85 data = zi.get_data(parts[1]) |
|
86 except IOError: |
|
87 continue |
|
88 return data |
|
89 return None |
|
90 |
|
91 |
|
92 class PythonFileReporter(FileReporter): |
|
93 """Report support for a Python file.""" |
|
94 |
|
95 def __init__(self, morf, coverage=None): |
|
96 self.coverage = coverage |
|
97 |
|
98 if hasattr(morf, '__file__'): |
|
99 filename = morf.__file__ |
|
100 elif isinstance(morf, types.ModuleType): |
|
101 # A module should have had .__file__, otherwise we can't use it. |
|
102 # This could be a PEP-420 namespace package. |
|
103 raise CoverageException("Module {0} has no file".format(morf)) |
|
104 else: |
|
105 filename = morf |
|
106 |
|
107 filename = files.unicode_filename(filename) |
|
108 |
|
109 # .pyc files should always refer to a .py instead. |
|
110 if filename.endswith(('.pyc', '.pyo')): |
|
111 filename = filename[:-1] |
|
112 elif filename.endswith('$py.class'): # Jython |
|
113 filename = filename[:-9] + ".py" |
|
114 |
|
115 super(PythonFileReporter, self).__init__(files.canonical_filename(filename)) |
|
116 |
|
117 if hasattr(morf, '__name__'): |
|
118 name = morf.__name__ |
|
119 name = name.replace(".", os.sep) + ".py" |
|
120 name = files.unicode_filename(name) |
|
121 else: |
|
122 name = files.relative_filename(filename) |
|
123 self.relname = name |
|
124 |
|
125 self._source = None |
|
126 self._parser = None |
|
127 self._statements = None |
|
128 self._excluded = None |
|
129 |
|
130 @contract(returns='unicode') |
|
131 def relative_filename(self): |
|
132 return self.relname |
|
133 |
|
134 @property |
|
135 def parser(self): |
|
136 """Lazily create a :class:`PythonParser`.""" |
|
137 if self._parser is None: |
|
138 self._parser = PythonParser( |
|
139 filename=self.filename, |
|
140 exclude=self.coverage._exclude_regex('exclude'), |
|
141 ) |
|
142 self._parser.parse_source() |
|
143 return self._parser |
|
144 |
|
145 def lines(self): |
|
146 """Return the line numbers of statements in the file.""" |
|
147 return self.parser.statements |
|
148 |
|
149 def excluded_lines(self): |
|
150 """Return the line numbers of statements in the file.""" |
|
151 return self.parser.excluded |
|
152 |
|
153 def translate_lines(self, lines): |
|
154 return self.parser.translate_lines(lines) |
|
155 |
|
156 def translate_arcs(self, arcs): |
|
157 return self.parser.translate_arcs(arcs) |
|
158 |
|
159 @expensive |
|
160 def no_branch_lines(self): |
|
161 no_branch = self.parser.lines_matching( |
|
162 join_regex(self.coverage.config.partial_list), |
|
163 join_regex(self.coverage.config.partial_always_list) |
|
164 ) |
|
165 return no_branch |
|
166 |
|
167 @expensive |
|
168 def arcs(self): |
|
169 return self.parser.arcs() |
|
170 |
|
171 @expensive |
|
172 def exit_counts(self): |
|
173 return self.parser.exit_counts() |
|
174 |
|
175 def missing_arc_description(self, start, end, executed_arcs=None): |
|
176 return self.parser.missing_arc_description(start, end, executed_arcs) |
|
177 |
|
178 @contract(returns='unicode') |
|
179 def source(self): |
|
180 if self._source is None: |
|
181 self._source = get_python_source(self.filename) |
|
182 return self._source |
|
183 |
|
184 def should_be_python(self): |
|
185 """Does it seem like this file should contain Python? |
|
186 |
|
187 This is used to decide if a file reported as part of the execution of |
|
188 a program was really likely to have contained Python in the first |
|
189 place. |
|
190 |
|
191 """ |
|
192 # Get the file extension. |
|
193 _, ext = os.path.splitext(self.filename) |
|
194 |
|
195 # Anything named *.py* should be Python. |
|
196 if ext.startswith('.py'): |
|
197 return True |
|
198 # A file with no extension should be Python. |
|
199 if not ext: |
|
200 return True |
|
201 # Everything else is probably not Python. |
|
202 return False |
|
203 |
|
204 def source_token_lines(self): |
|
205 return source_token_lines(self.source()) |
|
206 |
|
207 # |
|
208 # eflag: FileType = Python2 |