python.py 6.2 KB

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