python.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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. """Python source expertise for coverage.py"""
  4. import os.path
  5. import types
  6. import zipimport
  7. from coverage import env, files
  8. from coverage.misc import contract, expensive, isolate_module, join_regex
  9. from coverage.misc import CoverageException, NoSource
  10. from coverage.parser import PythonParser
  11. from coverage.phystokens import source_token_lines, source_encoding
  12. from coverage.plugin import FileReporter
  13. os = isolate_module(os)
  14. @contract(returns='bytes')
  15. def read_python_source(filename):
  16. """Read the Python source text from `filename`.
  17. Returns bytes.
  18. """
  19. with open(filename, "rb") as f:
  20. source = f.read()
  21. if env.IRONPYTHON:
  22. # IronPython reads Unicode strings even for "rb" files.
  23. source = bytes(source)
  24. return source.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
  25. @contract(returns='unicode')
  26. def get_python_source(filename):
  27. """Return the source code, as unicode."""
  28. base, ext = os.path.splitext(filename)
  29. if ext == ".py" and env.WINDOWS:
  30. exts = [".py", ".pyw"]
  31. else:
  32. exts = [ext]
  33. for ext in exts:
  34. try_filename = base + ext
  35. if os.path.exists(try_filename):
  36. # A regular text file: open it.
  37. source = read_python_source(try_filename)
  38. break
  39. # Maybe it's in a zip file?
  40. source = get_zip_bytes(try_filename)
  41. if source is not None:
  42. break
  43. else:
  44. # Couldn't find source.
  45. exc_msg = "No source for code: '%s'.\n" % (filename,)
  46. exc_msg += "Aborting report output, consider using -i."
  47. raise NoSource(exc_msg)
  48. # Replace \f because of http://bugs.python.org/issue19035
  49. source = source.replace(b'\f', b' ')
  50. source = source.decode(source_encoding(source), "replace")
  51. # Python code should always end with a line with a newline.
  52. if source and source[-1] != '\n':
  53. source += '\n'
  54. return source
  55. @contract(returns='bytes|None')
  56. def get_zip_bytes(filename):
  57. """Get data from `filename` if it is a zip file path.
  58. Returns the bytestring data read from the zip file, or None if no zip file
  59. could be found or `filename` isn't in it. The data returned will be
  60. an empty string if the file is empty.
  61. """
  62. markers = ['.zip'+os.sep, '.egg'+os.sep, '.pex'+os.sep]
  63. for marker in markers:
  64. if marker in filename:
  65. parts = filename.split(marker)
  66. try:
  67. zi = zipimport.zipimporter(parts[0]+marker[:-1])
  68. except zipimport.ZipImportError:
  69. continue
  70. try:
  71. data = zi.get_data(parts[1])
  72. except IOError:
  73. continue
  74. return data
  75. return None
  76. def source_for_file(filename):
  77. """Return the source file for `filename`.
  78. Given a file name being traced, return the best guess as to the source
  79. file to attribute it to.
  80. """
  81. if filename.endswith(".py"):
  82. # .py files are themselves source files.
  83. return filename
  84. elif filename.endswith((".pyc", ".pyo")):
  85. # Bytecode files probably have source files near them.
  86. py_filename = filename[:-1]
  87. if os.path.exists(py_filename):
  88. # Found a .py file, use that.
  89. return py_filename
  90. if env.WINDOWS:
  91. # On Windows, it could be a .pyw file.
  92. pyw_filename = py_filename + "w"
  93. if os.path.exists(pyw_filename):
  94. return pyw_filename
  95. # Didn't find source, but it's probably the .py file we want.
  96. return py_filename
  97. elif filename.endswith("$py.class"):
  98. # Jython is easy to guess.
  99. return filename[:-9] + ".py"
  100. # No idea, just use the file name as-is.
  101. return filename
  102. class PythonFileReporter(FileReporter):
  103. """Report support for a Python file."""
  104. def __init__(self, morf, coverage=None):
  105. self.coverage = coverage
  106. if hasattr(morf, '__file__'):
  107. filename = morf.__file__
  108. elif isinstance(morf, types.ModuleType):
  109. # A module should have had .__file__, otherwise we can't use it.
  110. # This could be a PEP-420 namespace package.
  111. raise CoverageException("Module {0} has no file".format(morf))
  112. else:
  113. filename = morf
  114. filename = source_for_file(files.unicode_filename(filename))
  115. super(PythonFileReporter, self).__init__(files.canonical_filename(filename))
  116. if hasattr(morf, '__name__'):
  117. name = morf.__name__.replace(".", os.sep)
  118. if os.path.basename(filename).startswith('__init__.'):
  119. name += os.sep + "__init__"
  120. name += ".py"
  121. name = files.unicode_filename(name)
  122. else:
  123. name = files.relative_filename(filename)
  124. self.relname = name
  125. self._source = None
  126. self._parser = None
  127. self._statements = None
  128. self._excluded = None
  129. def __repr__(self):
  130. return "<PythonFileReporter {0!r}>".format(self.filename)
  131. @contract(returns='unicode')
  132. def relative_filename(self):
  133. return self.relname
  134. @property
  135. def parser(self):
  136. """Lazily create a :class:`PythonParser`."""
  137. if self._parser is None:
  138. self._parser = PythonParser(
  139. filename=self.filename,
  140. exclude=self.coverage._exclude_regex('exclude'),
  141. )
  142. self._parser.parse_source()
  143. return self._parser
  144. def lines(self):
  145. """Return the line numbers of statements in the file."""
  146. return self.parser.statements
  147. def excluded_lines(self):
  148. """Return the line numbers of statements in the file."""
  149. return self.parser.excluded
  150. def translate_lines(self, lines):
  151. return self.parser.translate_lines(lines)
  152. def translate_arcs(self, arcs):
  153. return self.parser.translate_arcs(arcs)
  154. @expensive
  155. def no_branch_lines(self):
  156. no_branch = self.parser.lines_matching(
  157. join_regex(self.coverage.config.partial_list),
  158. join_regex(self.coverage.config.partial_always_list)
  159. )
  160. return no_branch
  161. @expensive
  162. def arcs(self):
  163. return self.parser.arcs()
  164. @expensive
  165. def exit_counts(self):
  166. return self.parser.exit_counts()
  167. def missing_arc_description(self, start, end, executed_arcs=None):
  168. return self.parser.missing_arc_description(start, end, executed_arcs)
  169. @contract(returns='unicode')
  170. def source(self):
  171. if self._source is None:
  172. self._source = get_python_source(self.filename)
  173. return self._source
  174. def should_be_python(self):
  175. """Does it seem like this file should contain Python?
  176. This is used to decide if a file reported as part of the execution of
  177. a program was really likely to have contained Python in the first
  178. place.
  179. """
  180. # Get the file extension.
  181. _, ext = os.path.splitext(self.filename)
  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. def source_token_lines(self):
  191. return source_token_lines(self.source())