6 import os.path |
6 import os.path |
7 import types |
7 import types |
8 import zipimport |
8 import zipimport |
9 |
9 |
10 from coverage import env, files |
10 from coverage import env, files |
11 from coverage.misc import ( |
11 from coverage.misc import contract, expensive, isolate_module, join_regex |
12 contract, CoverageException, expensive, NoSource, join_regex, isolate_module, |
12 from coverage.misc import CoverageException, NoSource |
13 ) |
|
14 from coverage.parser import PythonParser |
13 from coverage.parser import PythonParser |
15 from coverage.phystokens import source_token_lines, source_encoding |
14 from coverage.phystokens import source_token_lines, source_encoding |
16 from coverage.plugin import FileReporter |
15 from coverage.plugin import FileReporter |
17 |
16 |
18 os = isolate_module(os) |
17 os = isolate_module(os) |
24 |
23 |
25 Returns bytes. |
24 Returns bytes. |
26 |
25 |
27 """ |
26 """ |
28 with open(filename, "rb") as f: |
27 with open(filename, "rb") as f: |
29 return f.read().replace(b"\r\n", b"\n").replace(b"\r", b"\n") |
28 source = f.read() |
|
29 |
|
30 if env.IRONPYTHON: |
|
31 # IronPython reads Unicode strings even for "rb" files. |
|
32 source = bytes(source) |
|
33 |
|
34 return source.replace(b"\r\n", b"\n").replace(b"\r", b"\n") |
30 |
35 |
31 |
36 |
32 @contract(returns='unicode') |
37 @contract(returns='unicode') |
33 def get_python_source(filename): |
38 def get_python_source(filename): |
34 """Return the source code, as unicode.""" |
39 """Return the source code, as unicode.""" |
49 source = get_zip_bytes(try_filename) |
54 source = get_zip_bytes(try_filename) |
50 if source is not None: |
55 if source is not None: |
51 break |
56 break |
52 else: |
57 else: |
53 # Couldn't find source. |
58 # Couldn't find source. |
54 raise NoSource("No source for code: '%s'." % filename) |
59 exc_msg = "No source for code: '%s'.\n" % (filename,) |
|
60 exc_msg += "Aborting report output, consider using -i." |
|
61 raise NoSource(exc_msg) |
55 |
62 |
56 # Replace \f because of http://bugs.python.org/issue19035 |
63 # Replace \f because of http://bugs.python.org/issue19035 |
57 source = source.replace(b'\f', b' ') |
64 source = source.replace(b'\f', b' ') |
58 source = source.decode(source_encoding(source), "replace") |
65 source = source.decode(source_encoding(source), "replace") |
59 |
66 |
71 Returns the bytestring data read from the zip file, or None if no zip file |
78 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 |
79 could be found or `filename` isn't in it. The data returned will be |
73 an empty string if the file is empty. |
80 an empty string if the file is empty. |
74 |
81 |
75 """ |
82 """ |
76 markers = ['.zip'+os.sep, '.egg'+os.sep] |
83 markers = ['.zip'+os.sep, '.egg'+os.sep, '.pex'+os.sep] |
77 for marker in markers: |
84 for marker in markers: |
78 if marker in filename: |
85 if marker in filename: |
79 parts = filename.split(marker) |
86 parts = filename.split(marker) |
80 try: |
87 try: |
81 zi = zipimport.zipimporter(parts[0]+marker[:-1]) |
88 zi = zipimport.zipimporter(parts[0]+marker[:-1]) |
87 continue |
94 continue |
88 return data |
95 return data |
89 return None |
96 return None |
90 |
97 |
91 |
98 |
|
99 def source_for_file(filename): |
|
100 """Return the source file for `filename`. |
|
101 |
|
102 Given a file name being traced, return the best guess as to the source |
|
103 file to attribute it to. |
|
104 |
|
105 """ |
|
106 if filename.endswith(".py"): |
|
107 # .py files are themselves source files. |
|
108 return filename |
|
109 |
|
110 elif filename.endswith((".pyc", ".pyo")): |
|
111 # Bytecode files probably have source files near them. |
|
112 py_filename = filename[:-1] |
|
113 if os.path.exists(py_filename): |
|
114 # Found a .py file, use that. |
|
115 return py_filename |
|
116 if env.WINDOWS: |
|
117 # On Windows, it could be a .pyw file. |
|
118 pyw_filename = py_filename + "w" |
|
119 if os.path.exists(pyw_filename): |
|
120 return pyw_filename |
|
121 # Didn't find source, but it's probably the .py file we want. |
|
122 return py_filename |
|
123 |
|
124 elif filename.endswith("$py.class"): |
|
125 # Jython is easy to guess. |
|
126 return filename[:-9] + ".py" |
|
127 |
|
128 # No idea, just use the file name as-is. |
|
129 return filename |
|
130 |
|
131 |
92 class PythonFileReporter(FileReporter): |
132 class PythonFileReporter(FileReporter): |
93 """Report support for a Python file.""" |
133 """Report support for a Python file.""" |
94 |
134 |
95 def __init__(self, morf, coverage=None): |
135 def __init__(self, morf, coverage=None): |
96 self.coverage = coverage |
136 self.coverage = coverage |
102 # This could be a PEP-420 namespace package. |
142 # This could be a PEP-420 namespace package. |
103 raise CoverageException("Module {0} has no file".format(morf)) |
143 raise CoverageException("Module {0} has no file".format(morf)) |
104 else: |
144 else: |
105 filename = morf |
145 filename = morf |
106 |
146 |
107 filename = files.unicode_filename(filename) |
147 filename = source_for_file(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 |
148 |
115 super(PythonFileReporter, self).__init__(files.canonical_filename(filename)) |
149 super(PythonFileReporter, self).__init__(files.canonical_filename(filename)) |
116 |
150 |
117 if hasattr(morf, '__name__'): |
151 if hasattr(morf, '__name__'): |
118 name = morf.__name__ |
152 name = morf.__name__.replace(".", os.sep) |
119 name = name.replace(".", os.sep) + ".py" |
153 if os.path.basename(filename).startswith('__init__.'): |
|
154 name += os.sep + "__init__" |
|
155 name += ".py" |
120 name = files.unicode_filename(name) |
156 name = files.unicode_filename(name) |
121 else: |
157 else: |
122 name = files.relative_filename(filename) |
158 name = files.relative_filename(filename) |
123 self.relname = name |
159 self.relname = name |
124 |
160 |
125 self._source = None |
161 self._source = None |
126 self._parser = None |
162 self._parser = None |
127 self._statements = None |
163 self._statements = None |
128 self._excluded = None |
164 self._excluded = None |
|
165 |
|
166 def __repr__(self): |
|
167 return "<PythonFileReporter {0!r}>".format(self.filename) |
129 |
168 |
130 @contract(returns='unicode') |
169 @contract(returns='unicode') |
131 def relative_filename(self): |
170 def relative_filename(self): |
132 return self.relname |
171 return self.relname |
133 |
172 |