DebugClients/Python3/coverage/python.py

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

eric ide

mercurial