src/eric7/DebugClients/Python/coverage/python.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9099
0e511e0e94a3
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
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
11 from coverage.exceptions import CoverageException, NoSource
12 from coverage.files import canonical_filename, relative_filename
13 from coverage.misc import contract, expensive, isolate_module, join_regex
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 source = f.read()
30
31 if env.IRONPYTHON:
32 # IronPython reads Unicode strings even for "rb" files.
33 source = bytes(source)
34
35 return source.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
36
37
38 @contract(returns='unicode')
39 def get_python_source(filename):
40 """Return the source code, as unicode."""
41 base, ext = os.path.splitext(filename)
42 if ext == ".py" and env.WINDOWS:
43 exts = [".py", ".pyw"]
44 else:
45 exts = [ext]
46
47 for ext in exts:
48 try_filename = base + ext
49 if os.path.exists(try_filename):
50 # A regular text file: open it.
51 source = read_python_source(try_filename)
52 break
53
54 # Maybe it's in a zip file?
55 source = get_zip_bytes(try_filename)
56 if source is not None:
57 break
58 else:
59 # Couldn't find source.
60 raise NoSource(f"No source for code: '{filename}'.")
61
62 # Replace \f because of http://bugs.python.org/issue19035
63 source = source.replace(b'\f', b' ')
64 source = source.decode(source_encoding(source), "replace")
65
66 # Python code should always end with a line with a newline.
67 if source and source[-1] != '\n':
68 source += '\n'
69
70 return source
71
72
73 @contract(returns='bytes|None')
74 def get_zip_bytes(filename):
75 """Get data from `filename` if it is a zip file path.
76
77 Returns the bytestring data read from the zip file, or None if no zip file
78 could be found or `filename` isn't in it. The data returned will be
79 an empty string if the file is empty.
80
81 """
82 markers = ['.zip'+os.sep, '.egg'+os.sep, '.pex'+os.sep]
83 for marker in markers:
84 if marker in filename:
85 parts = filename.split(marker)
86 try:
87 zi = zipimport.zipimporter(parts[0]+marker[:-1])
88 except zipimport.ZipImportError:
89 continue
90 try:
91 data = zi.get_data(parts[1])
92 except OSError:
93 continue
94 return data
95 return None
96
97
98 def source_for_file(filename):
99 """Return the source filename for `filename`.
100
101 Given a file name being traced, return the best guess as to the source
102 file to attribute it to.
103
104 """
105 if filename.endswith(".py"):
106 # .py files are themselves source files.
107 return filename
108
109 elif filename.endswith((".pyc", ".pyo")):
110 # Bytecode files probably have source files near them.
111 py_filename = filename[:-1]
112 if os.path.exists(py_filename):
113 # Found a .py file, use that.
114 return py_filename
115 if env.WINDOWS:
116 # On Windows, it could be a .pyw file.
117 pyw_filename = py_filename + "w"
118 if os.path.exists(pyw_filename):
119 return pyw_filename
120 # Didn't find source, but it's probably the .py file we want.
121 return py_filename
122
123 elif filename.endswith("$py.class"):
124 # Jython is easy to guess.
125 return filename[:-9] + ".py"
126
127 # No idea, just use the file name as-is.
128 return filename
129
130
131 def source_for_morf(morf):
132 """Get the source filename for the module-or-file `morf`."""
133 if hasattr(morf, '__file__') and morf.__file__:
134 filename = morf.__file__
135 elif isinstance(morf, types.ModuleType):
136 # A module should have had .__file__, otherwise we can't use it.
137 # This could be a PEP-420 namespace package.
138 raise CoverageException(f"Module {morf} has no file")
139 else:
140 filename = morf
141
142 filename = source_for_file(filename)
143 return filename
144
145
146 class PythonFileReporter(FileReporter):
147 """Report support for a Python file."""
148
149 def __init__(self, morf, coverage=None):
150 self.coverage = coverage
151
152 filename = source_for_morf(morf)
153
154 super().__init__(canonical_filename(filename))
155
156 if hasattr(morf, '__name__'):
157 name = morf.__name__.replace(".", os.sep)
158 if os.path.basename(filename).startswith('__init__.'):
159 name += os.sep + "__init__"
160 name += ".py"
161 else:
162 name = relative_filename(filename)
163 self.relname = name
164
165 self._source = None
166 self._parser = None
167 self._excluded = None
168
169 def __repr__(self):
170 return f"<PythonFileReporter {self.filename!r}>"
171
172 @contract(returns='unicode')
173 def relative_filename(self):
174 return self.relname
175
176 @property
177 def parser(self):
178 """Lazily create a :class:`PythonParser`."""
179 if self._parser is None:
180 self._parser = PythonParser(
181 filename=self.filename,
182 exclude=self.coverage._exclude_regex('exclude'),
183 )
184 self._parser.parse_source()
185 return self._parser
186
187 def lines(self):
188 """Return the line numbers of statements in the file."""
189 return self.parser.statements
190
191 def excluded_lines(self):
192 """Return the line numbers of statements in the file."""
193 return self.parser.excluded
194
195 def translate_lines(self, lines):
196 return self.parser.translate_lines(lines)
197
198 def translate_arcs(self, arcs):
199 return self.parser.translate_arcs(arcs)
200
201 @expensive
202 def no_branch_lines(self):
203 no_branch = self.parser.lines_matching(
204 join_regex(self.coverage.config.partial_list),
205 join_regex(self.coverage.config.partial_always_list),
206 )
207 return no_branch
208
209 @expensive
210 def arcs(self):
211 return self.parser.arcs()
212
213 @expensive
214 def exit_counts(self):
215 return self.parser.exit_counts()
216
217 def missing_arc_description(self, start, end, executed_arcs=None):
218 return self.parser.missing_arc_description(start, end, executed_arcs)
219
220 @contract(returns='unicode')
221 def source(self):
222 if self._source is None:
223 self._source = get_python_source(self.filename)
224 return self._source
225
226 def should_be_python(self):
227 """Does it seem like this file should contain Python?
228
229 This is used to decide if a file reported as part of the execution of
230 a program was really likely to have contained Python in the first
231 place.
232
233 """
234 # Get the file extension.
235 _, ext = os.path.splitext(self.filename)
236
237 # Anything named *.py* should be Python.
238 if ext.startswith('.py'):
239 return True
240 # A file with no extension should be Python.
241 if not ext:
242 return True
243 # Everything else is probably not Python.
244 return False
245
246 def source_token_lines(self):
247 return source_token_lines(self.source())

eric ide

mercurial