eric7/DebugClients/Python/coverage/python.py

branch
eric7
changeset 8312
800c432b34c8
parent 7427
362cd1b6f81a
child 8775
0802ae193343
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
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 """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 contract, expensive, isolate_module, join_regex
12 from coverage.misc import CoverageException, NoSource
13 from coverage.parser import PythonParser
14 from coverage.phystokens import source_token_lines, source_encoding
15 from coverage.plugin import FileReporter
16
17 os = isolate_module(os)
18
19
20 @contract(returns='bytes')
21 def read_python_source(filename):
22 """Read the Python source text from `filename`.
23
24 Returns bytes.
25
26 """
27 with open(filename, "rb") as f:
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")
35
36
37 @contract(returns='unicode')
38 def get_python_source(filename):
39 """Return the source code, as unicode."""
40 base, ext = os.path.splitext(filename)
41 if ext == ".py" and env.WINDOWS:
42 exts = [".py", ".pyw"]
43 else:
44 exts = [ext]
45
46 for ext in exts:
47 try_filename = base + ext
48 if os.path.exists(try_filename):
49 # A regular text file: open it.
50 source = read_python_source(try_filename)
51 break
52
53 # Maybe it's in a zip file?
54 source = get_zip_bytes(try_filename)
55 if source is not None:
56 break
57 else:
58 # Couldn't find source.
59 exc_msg = "No source for code: '%s'.\n" % (filename,)
60 exc_msg += "Aborting report output, consider using -i."
61 raise NoSource(exc_msg)
62
63 # Replace \f because of http://bugs.python.org/issue19035
64 source = source.replace(b'\f', b' ')
65 source = source.decode(source_encoding(source), "replace")
66
67 # Python code should always end with a line with a newline.
68 if source and source[-1] != '\n':
69 source += '\n'
70
71 return source
72
73
74 @contract(returns='bytes|None')
75 def get_zip_bytes(filename):
76 """Get data from `filename` if it is a zip file path.
77
78 Returns the bytestring data read from the zip file, or None if no zip file
79 could be found or `filename` isn't in it. The data returned will be
80 an empty string if the file is empty.
81
82 """
83 markers = ['.zip'+os.sep, '.egg'+os.sep, '.pex'+os.sep]
84 for marker in markers:
85 if marker in filename:
86 parts = filename.split(marker)
87 try:
88 zi = zipimport.zipimporter(parts[0]+marker[:-1])
89 except zipimport.ZipImportError:
90 continue
91 try:
92 data = zi.get_data(parts[1])
93 except IOError:
94 continue
95 return data
96 return None
97
98
99 def source_for_file(filename):
100 """Return the source filename 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
132 def source_for_morf(morf):
133 """Get the source filename for the module-or-file `morf`."""
134 if hasattr(morf, '__file__') and morf.__file__:
135 filename = morf.__file__
136 elif isinstance(morf, types.ModuleType):
137 # A module should have had .__file__, otherwise we can't use it.
138 # This could be a PEP-420 namespace package.
139 raise CoverageException("Module {} has no file".format(morf))
140 else:
141 filename = morf
142
143 filename = source_for_file(files.unicode_filename(filename))
144 return filename
145
146
147 class PythonFileReporter(FileReporter):
148 """Report support for a Python file."""
149
150 def __init__(self, morf, coverage=None):
151 self.coverage = coverage
152
153 filename = source_for_morf(morf)
154
155 super(PythonFileReporter, self).__init__(files.canonical_filename(filename))
156
157 if hasattr(morf, '__name__'):
158 name = morf.__name__.replace(".", os.sep)
159 if os.path.basename(filename).startswith('__init__.'):
160 name += os.sep + "__init__"
161 name += ".py"
162 name = files.unicode_filename(name)
163 else:
164 name = files.relative_filename(filename)
165 self.relname = name
166
167 self._source = None
168 self._parser = None
169 self._excluded = None
170
171 def __repr__(self):
172 return "<PythonFileReporter {!r}>".format(self.filename)
173
174 @contract(returns='unicode')
175 def relative_filename(self):
176 return self.relname
177
178 @property
179 def parser(self):
180 """Lazily create a :class:`PythonParser`."""
181 if self._parser is None:
182 self._parser = PythonParser(
183 filename=self.filename,
184 exclude=self.coverage._exclude_regex('exclude'),
185 )
186 self._parser.parse_source()
187 return self._parser
188
189 def lines(self):
190 """Return the line numbers of statements in the file."""
191 return self.parser.statements
192
193 def excluded_lines(self):
194 """Return the line numbers of statements in the file."""
195 return self.parser.excluded
196
197 def translate_lines(self, lines):
198 return self.parser.translate_lines(lines)
199
200 def translate_arcs(self, arcs):
201 return self.parser.translate_arcs(arcs)
202
203 @expensive
204 def no_branch_lines(self):
205 no_branch = self.parser.lines_matching(
206 join_regex(self.coverage.config.partial_list),
207 join_regex(self.coverage.config.partial_always_list)
208 )
209 return no_branch
210
211 @expensive
212 def arcs(self):
213 return self.parser.arcs()
214
215 @expensive
216 def exit_counts(self):
217 return self.parser.exit_counts()
218
219 def missing_arc_description(self, start, end, executed_arcs=None):
220 return self.parser.missing_arc_description(start, end, executed_arcs)
221
222 @contract(returns='unicode')
223 def source(self):
224 if self._source is None:
225 self._source = get_python_source(self.filename)
226 return self._source
227
228 def should_be_python(self):
229 """Does it seem like this file should contain Python?
230
231 This is used to decide if a file reported as part of the execution of
232 a program was really likely to have contained Python in the first
233 place.
234
235 """
236 # Get the file extension.
237 _, ext = os.path.splitext(self.filename)
238
239 # Anything named *.py* should be Python.
240 if ext.startswith('.py'):
241 return True
242 # A file with no extension should be Python.
243 if not ext:
244 return True
245 # Everything else is probably not Python.
246 return False
247
248 def source_token_lines(self):
249 return source_token_lines(self.source())

eric ide

mercurial