plugin.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  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. """Plugin interfaces for coverage.py"""
  4. from coverage import files
  5. from coverage.misc import contract, _needs_to_implement
  6. class CoveragePlugin(object):
  7. """Base class for coverage.py plugins.
  8. To write a coverage.py plugin, create a module with a subclass of
  9. :class:`CoveragePlugin`. You will override methods in your class to
  10. participate in various aspects of coverage.py's processing.
  11. Currently the only plugin type is a file tracer, for implementing
  12. measurement support for non-Python files. File tracer plugins implement
  13. the :meth:`file_tracer` method to claim files and the :meth:`file_reporter`
  14. method to report on those files.
  15. Any plugin can optionally implement :meth:`sys_info` to provide debugging
  16. information about their operation.
  17. Coverage.py will store its own information on your plugin object, using
  18. attributes whose names start with ``_coverage_``. Don't be startled.
  19. To register your plugin, define a function called `coverage_init` in your
  20. module::
  21. def coverage_init(reg, options):
  22. reg.add_file_tracer(MyPlugin())
  23. You use the `reg` parameter passed to your `coverage_init` function to
  24. register your plugin object. It has one method, `add_file_tracer`, which
  25. takes a newly created instance of your plugin.
  26. If your plugin takes options, the `options` parameter is a dictionary of
  27. your plugin's options from the coverage.py configuration file. Use them
  28. however you want to configure your object before registering it.
  29. """
  30. def file_tracer(self, filename): # pylint: disable=unused-argument
  31. """Get a :class:`FileTracer` object for a file.
  32. Every Python source file is offered to the plugin to give it a chance
  33. to take responsibility for tracing the file. If your plugin can handle
  34. the file, then return a :class:`FileTracer` object. Otherwise return
  35. None.
  36. There is no way to register your plugin for particular files. Instead,
  37. this method is invoked for all files, and the plugin decides whether it
  38. can trace the file or not. Be prepared for `filename` to refer to all
  39. kinds of files that have nothing to do with your plugin.
  40. The file name will be a Python file being executed. There are two
  41. broad categories of behavior for a plugin, depending on the kind of
  42. files your plugin supports:
  43. * Static file names: each of your original source files has been
  44. converted into a distinct Python file. Your plugin is invoked with
  45. the Python file name, and it maps it back to its original source
  46. file.
  47. * Dynamic file names: all of your source files are executed by the same
  48. Python file. In this case, your plugin implements
  49. :meth:`FileTracer.dynamic_source_filename` to provide the actual
  50. source file for each execution frame.
  51. `filename` is a string, the path to the file being considered. This is
  52. the absolute real path to the file. If you are comparing to other
  53. paths, be sure to take this into account.
  54. Returns a :class:`FileTracer` object to use to trace `filename`, or
  55. None if this plugin cannot trace this file.
  56. """
  57. return None
  58. def file_reporter(self, filename): # pylint: disable=unused-argument
  59. """Get the :class:`FileReporter` class to use for a file.
  60. This will only be invoked if `filename` returns non-None from
  61. :meth:`file_tracer`. It's an error to return None from this method.
  62. Returns a :class:`FileReporter` object to use to report on `filename`.
  63. """
  64. _needs_to_implement(self, "file_reporter")
  65. def find_executable_files(self, src_dir): # pylint: disable=unused-argument
  66. """Yield all of the executable files in `src_dir`, recursively.
  67. Executability is a plugin-specific property, but generally means files
  68. which would have been considered for coverage analysis, had they been
  69. included automatically.
  70. Returns or yields a sequence of strings, the paths to files that could
  71. have been executed, including files that had been executed.
  72. """
  73. return []
  74. def sys_info(self):
  75. """Get a list of information useful for debugging.
  76. This method will be invoked for ``--debug=sys``. Your
  77. plugin can return any information it wants to be displayed.
  78. Returns a list of pairs: `[(name, value), ...]`.
  79. """
  80. return []
  81. class FileTracer(object):
  82. """Support needed for files during the execution phase.
  83. You may construct this object from :meth:`CoveragePlugin.file_tracer` any
  84. way you like. A natural choice would be to pass the file name given to
  85. `file_tracer`.
  86. `FileTracer` objects should only be created in the
  87. :meth:`CoveragePlugin.file_tracer` method.
  88. See :ref:`howitworks` for details of the different coverage.py phases.
  89. """
  90. def source_filename(self):
  91. """The source file name for this file.
  92. This may be any file name you like. A key responsibility of a plugin
  93. is to own the mapping from Python execution back to whatever source
  94. file name was originally the source of the code.
  95. See :meth:`CoveragePlugin.file_tracer` for details about static and
  96. dynamic file names.
  97. Returns the file name to credit with this execution.
  98. """
  99. _needs_to_implement(self, "source_filename")
  100. def has_dynamic_source_filename(self):
  101. """Does this FileTracer have dynamic source file names?
  102. FileTracers can provide dynamically determined file names by
  103. implementing :meth:`dynamic_source_filename`. Invoking that function
  104. is expensive. To determine whether to invoke it, coverage.py uses the
  105. result of this function to know if it needs to bother invoking
  106. :meth:`dynamic_source_filename`.
  107. See :meth:`CoveragePlugin.file_tracer` for details about static and
  108. dynamic file names.
  109. Returns True if :meth:`dynamic_source_filename` should be called to get
  110. dynamic source file names.
  111. """
  112. return False
  113. def dynamic_source_filename(self, filename, frame): # pylint: disable=unused-argument
  114. """Get a dynamically computed source file name.
  115. Some plugins need to compute the source file name dynamically for each
  116. frame.
  117. This function will not be invoked if
  118. :meth:`has_dynamic_source_filename` returns False.
  119. Returns the source file name for this frame, or None if this frame
  120. shouldn't be measured.
  121. """
  122. return None
  123. def line_number_range(self, frame):
  124. """Get the range of source line numbers for a given a call frame.
  125. The call frame is examined, and the source line number in the original
  126. file is returned. The return value is a pair of numbers, the starting
  127. line number and the ending line number, both inclusive. For example,
  128. returning (5, 7) means that lines 5, 6, and 7 should be considered
  129. executed.
  130. This function might decide that the frame doesn't indicate any lines
  131. from the source file were executed. Return (-1, -1) in this case to
  132. tell coverage.py that no lines should be recorded for this frame.
  133. """
  134. lineno = frame.f_lineno
  135. return lineno, lineno
  136. class FileReporter(object):
  137. """Support needed for files during the analysis and reporting phases.
  138. See :ref:`howitworks` for details of the different coverage.py phases.
  139. `FileReporter` objects should only be created in the
  140. :meth:`CoveragePlugin.file_reporter` method.
  141. There are many methods here, but only :meth:`lines` is required, to provide
  142. the set of executable lines in the file.
  143. """
  144. def __init__(self, filename):
  145. """Simple initialization of a `FileReporter`.
  146. The `filename` argument is the path to the file being reported. This
  147. will be available as the `.filename` attribute on the object. Other
  148. method implementations on this base class rely on this attribute.
  149. """
  150. self.filename = filename
  151. def __repr__(self):
  152. return "<{0.__class__.__name__} filename={0.filename!r}>".format(self)
  153. def relative_filename(self):
  154. """Get the relative file name for this file.
  155. This file path will be displayed in reports. The default
  156. implementation will supply the actual project-relative file path. You
  157. only need to supply this method if you have an unusual syntax for file
  158. paths.
  159. """
  160. return files.relative_filename(self.filename)
  161. @contract(returns='unicode')
  162. def source(self):
  163. """Get the source for the file.
  164. Returns a Unicode string.
  165. The base implementation simply reads the `self.filename` file and
  166. decodes it as UTF8. Override this method if your file isn't readable
  167. as a text file, or if you need other encoding support.
  168. """
  169. with open(self.filename, "rb") as f:
  170. return f.read().decode("utf8")
  171. def lines(self):
  172. """Get the executable lines in this file.
  173. Your plugin must determine which lines in the file were possibly
  174. executable. This method returns a set of those line numbers.
  175. Returns a set of line numbers.
  176. """
  177. _needs_to_implement(self, "lines")
  178. def excluded_lines(self):
  179. """Get the excluded executable lines in this file.
  180. Your plugin can use any method it likes to allow the user to exclude
  181. executable lines from consideration.
  182. Returns a set of line numbers.
  183. The base implementation returns the empty set.
  184. """
  185. return set()
  186. def translate_lines(self, lines):
  187. """Translate recorded lines into reported lines.
  188. Some file formats will want to report lines slightly differently than
  189. they are recorded. For example, Python records the last line of a
  190. multi-line statement, but reports are nicer if they mention the first
  191. line.
  192. Your plugin can optionally define this method to perform these kinds of
  193. adjustment.
  194. `lines` is a sequence of integers, the recorded line numbers.
  195. Returns a set of integers, the adjusted line numbers.
  196. The base implementation returns the numbers unchanged.
  197. """
  198. return set(lines)
  199. def arcs(self):
  200. """Get the executable arcs in this file.
  201. To support branch coverage, your plugin needs to be able to indicate
  202. possible execution paths, as a set of line number pairs. Each pair is
  203. a `(prev, next)` pair indicating that execution can transition from the
  204. `prev` line number to the `next` line number.
  205. Returns a set of pairs of line numbers. The default implementation
  206. returns an empty set.
  207. """
  208. return set()
  209. def no_branch_lines(self):
  210. """Get the lines excused from branch coverage in this file.
  211. Your plugin can use any method it likes to allow the user to exclude
  212. lines from consideration of branch coverage.
  213. Returns a set of line numbers.
  214. The base implementation returns the empty set.
  215. """
  216. return set()
  217. def translate_arcs(self, arcs):
  218. """Translate recorded arcs into reported arcs.
  219. Similar to :meth:`translate_lines`, but for arcs. `arcs` is a set of
  220. line number pairs.
  221. Returns a set of line number pairs.
  222. The default implementation returns `arcs` unchanged.
  223. """
  224. return arcs
  225. def exit_counts(self):
  226. """Get a count of exits from that each line.
  227. To determine which lines are branches, coverage.py looks for lines that
  228. have more than one exit. This function creates a dict mapping each
  229. executable line number to a count of how many exits it has.
  230. To be honest, this feels wrong, and should be refactored. Let me know
  231. if you attempt to implement this method in your plugin...
  232. """
  233. return {}
  234. def missing_arc_description(self, start, end, executed_arcs=None): # pylint: disable=unused-argument
  235. """Provide an English sentence describing a missing arc.
  236. The `start` and `end` arguments are the line numbers of the missing
  237. arc. Negative numbers indicate entering or exiting code objects.
  238. The `executed_arcs` argument is a set of line number pairs, the arcs
  239. that were executed in this file.
  240. By default, this simply returns the string "Line {start} didn't jump
  241. to {end}".
  242. """
  243. return "Line {start} didn't jump to line {end}".format(start=start, end=end)
  244. def source_token_lines(self):
  245. """Generate a series of tokenized lines, one for each line in `source`.
  246. These tokens are used for syntax-colored reports.
  247. Each line is a list of pairs, each pair is a token::
  248. [('key', 'def'), ('ws', ' '), ('nam', 'hello'), ('op', '('), ... ]
  249. Each pair has a token class, and the token text. The token classes
  250. are:
  251. * ``'com'``: a comment
  252. * ``'key'``: a keyword
  253. * ``'nam'``: a name, or identifier
  254. * ``'num'``: a number
  255. * ``'op'``: an operator
  256. * ``'str'``: a string literal
  257. * ``'txt'``: some other kind of text
  258. If you concatenate all the token texts, and then join them with
  259. newlines, you should have your original source back.
  260. The default implementation simply returns each line tagged as
  261. ``'txt'``.
  262. """
  263. for line in self.source().splitlines():
  264. yield [('txt', line)]
  265. # Annoying comparison operators. Py3k wants __lt__ etc, and Py2k needs all
  266. # of them defined.
  267. def __eq__(self, other):
  268. return isinstance(other, FileReporter) and self.filename == other.filename
  269. def __ne__(self, other):
  270. return not (self == other)
  271. def __lt__(self, other):
  272. return self.filename < other.filename
  273. def __le__(self, other):
  274. return self.filename <= other.filename
  275. def __gt__(self, other):
  276. return self.filename > other.filename
  277. def __ge__(self, other):
  278. return self.filename >= other.filename
  279. __hash__ = None # This object doesn't need to be hashed.