2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt |
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt |
3 |
3 |
4 """Python source expertise for coverage.py""" |
4 """Python source expertise for coverage.py""" |
5 |
5 |
6 import os.path |
6 import os.path |
|
7 import types |
7 import zipimport |
8 import zipimport |
8 |
9 |
9 from coverage import env, files |
10 from coverage import env, files |
10 from coverage.misc import contract, expensive, NoSource, join_regex |
11 from coverage.misc import ( |
|
12 contract, CoverageException, expensive, NoSource, join_regex, isolate_module, |
|
13 ) |
11 from coverage.parser import PythonParser |
14 from coverage.parser import PythonParser |
12 from coverage.phystokens import source_token_lines, source_encoding |
15 from coverage.phystokens import source_token_lines, source_encoding |
13 from coverage.plugin import FileReporter |
16 from coverage.plugin import FileReporter |
|
17 |
|
18 os = isolate_module(os) |
14 |
19 |
15 |
20 |
16 @contract(returns='bytes') |
21 @contract(returns='bytes') |
17 def read_python_source(filename): |
22 def read_python_source(filename): |
18 """Read the Python source text from `filename`. |
23 """Read the Python source text from `filename`. |
46 break |
51 break |
47 else: |
52 else: |
48 # Couldn't find source. |
53 # Couldn't find source. |
49 raise NoSource("No source for code: '%s'." % filename) |
54 raise NoSource("No source for code: '%s'." % filename) |
50 |
55 |
|
56 # Replace \f because of http://bugs.python.org/issue19035 |
|
57 source = source.replace(b'\f', b' ') |
51 source = source.decode(source_encoding(source), "replace") |
58 source = source.decode(source_encoding(source), "replace") |
52 |
59 |
53 # Python code should always end with a line with a newline. |
60 # Python code should always end with a line with a newline. |
54 if source and source[-1] != '\n': |
61 if source and source[-1] != '\n': |
55 source += '\n' |
62 source += '\n' |
88 def __init__(self, morf, coverage=None): |
95 def __init__(self, morf, coverage=None): |
89 self.coverage = coverage |
96 self.coverage = coverage |
90 |
97 |
91 if hasattr(morf, '__file__'): |
98 if hasattr(morf, '__file__'): |
92 filename = 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)) |
93 else: |
104 else: |
94 filename = morf |
105 filename = morf |
|
106 |
|
107 filename = files.unicode_filename(filename) |
95 |
108 |
96 # .pyc files should always refer to a .py instead. |
109 # .pyc files should always refer to a .py instead. |
97 if filename.endswith(('.pyc', '.pyo')): |
110 if filename.endswith(('.pyc', '.pyo')): |
98 filename = filename[:-1] |
111 filename = filename[:-1] |
99 elif filename.endswith('$py.class'): # Jython |
112 elif filename.endswith('$py.class'): # Jython |
102 super(PythonFileReporter, self).__init__(files.canonical_filename(filename)) |
115 super(PythonFileReporter, self).__init__(files.canonical_filename(filename)) |
103 |
116 |
104 if hasattr(morf, '__name__'): |
117 if hasattr(morf, '__name__'): |
105 name = morf.__name__ |
118 name = morf.__name__ |
106 name = name.replace(".", os.sep) + ".py" |
119 name = name.replace(".", os.sep) + ".py" |
|
120 name = files.unicode_filename(name) |
107 else: |
121 else: |
108 name = files.relative_filename(filename) |
122 name = files.relative_filename(filename) |
109 self.relname = name |
123 self.relname = name |
110 |
124 |
111 self._source = None |
125 self._source = None |
112 self._parser = None |
126 self._parser = None |
113 self._statements = None |
127 self._statements = None |
114 self._excluded = None |
128 self._excluded = None |
115 |
129 |
|
130 @contract(returns='unicode') |
116 def relative_filename(self): |
131 def relative_filename(self): |
117 return self.relname |
132 return self.relname |
118 |
133 |
119 @property |
134 @property |
120 def parser(self): |
135 def parser(self): |
122 if self._parser is None: |
137 if self._parser is None: |
123 self._parser = PythonParser( |
138 self._parser = PythonParser( |
124 filename=self.filename, |
139 filename=self.filename, |
125 exclude=self.coverage._exclude_regex('exclude'), |
140 exclude=self.coverage._exclude_regex('exclude'), |
126 ) |
141 ) |
|
142 self._parser.parse_source() |
127 return self._parser |
143 return self._parser |
128 |
144 |
129 @expensive |
|
130 def lines(self): |
145 def lines(self): |
131 """Return the line numbers of statements in the file.""" |
146 """Return the line numbers of statements in the file.""" |
132 if self._statements is None: |
147 return self.parser.statements |
133 self._statements, self._excluded = self.parser.parse_source() |
148 |
134 return self._statements |
|
135 |
|
136 @expensive |
|
137 def excluded_lines(self): |
149 def excluded_lines(self): |
138 """Return the line numbers of statements in the file.""" |
150 """Return the line numbers of statements in the file.""" |
139 if self._excluded is None: |
151 return self.parser.excluded |
140 self._statements, self._excluded = self.parser.parse_source() |
|
141 return self._excluded |
|
142 |
152 |
143 def translate_lines(self, lines): |
153 def translate_lines(self, lines): |
144 return self.parser.translate_lines(lines) |
154 return self.parser.translate_lines(lines) |
145 |
155 |
146 def translate_arcs(self, arcs): |
156 def translate_arcs(self, arcs): |
159 return self.parser.arcs() |
169 return self.parser.arcs() |
160 |
170 |
161 @expensive |
171 @expensive |
162 def exit_counts(self): |
172 def exit_counts(self): |
163 return self.parser.exit_counts() |
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) |
164 |
177 |
165 @contract(returns='unicode') |
178 @contract(returns='unicode') |
166 def source(self): |
179 def source(self): |
167 if self._source is None: |
180 if self._source is None: |
168 self._source = get_python_source(self.filename) |
181 self._source = get_python_source(self.filename) |