base.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. """The base class and interface for all formatting plugins."""
  2. from __future__ import annotations
  3. import argparse
  4. import os
  5. import sys
  6. from typing import IO
  7. from flake8.formatting import _windows_color
  8. from flake8.statistics import Statistics
  9. from flake8.violation import Violation
  10. class BaseFormatter:
  11. """Class defining the formatter interface.
  12. .. attribute:: options
  13. The options parsed from both configuration files and the command-line.
  14. .. attribute:: filename
  15. If specified by the user, the path to store the results of the run.
  16. .. attribute:: output_fd
  17. Initialized when the :meth:`start` is called. This will be a file
  18. object opened for writing.
  19. .. attribute:: newline
  20. The string to add to the end of a line. This is only used when the
  21. output filename has been specified.
  22. """
  23. def __init__(self, options: argparse.Namespace) -> None:
  24. """Initialize with the options parsed from config and cli.
  25. This also calls a hook, :meth:`after_init`, so subclasses do not need
  26. to call super to call this method.
  27. :param options:
  28. User specified configuration parsed from both configuration files
  29. and the command-line interface.
  30. """
  31. self.options = options
  32. self.filename = options.output_file
  33. self.output_fd: IO[str] | None = None
  34. self.newline = "\n"
  35. self.color = options.color == "always" or (
  36. options.color == "auto"
  37. and sys.stdout.isatty()
  38. and _windows_color.terminal_supports_color
  39. )
  40. self.after_init()
  41. def after_init(self) -> None:
  42. """Initialize the formatter further."""
  43. def beginning(self, filename: str) -> None:
  44. """Notify the formatter that we're starting to process a file.
  45. :param filename:
  46. The name of the file that Flake8 is beginning to report results
  47. from.
  48. """
  49. def finished(self, filename: str) -> None:
  50. """Notify the formatter that we've finished processing a file.
  51. :param filename:
  52. The name of the file that Flake8 has finished reporting results
  53. from.
  54. """
  55. def start(self) -> None:
  56. """Prepare the formatter to receive input.
  57. This defaults to initializing :attr:`output_fd` if :attr:`filename`
  58. """
  59. if self.filename:
  60. dirname = os.path.dirname(os.path.abspath(self.filename))
  61. os.makedirs(dirname, exist_ok=True)
  62. self.output_fd = open(self.filename, "a")
  63. def handle(self, error: Violation) -> None:
  64. """Handle an error reported by Flake8.
  65. This defaults to calling :meth:`format`, :meth:`show_source`, and
  66. then :meth:`write`. To extend how errors are handled, override this
  67. method.
  68. :param error:
  69. This will be an instance of
  70. :class:`~flake8.violation.Violation`.
  71. """
  72. line = self.format(error)
  73. source = self.show_source(error)
  74. self.write(line, source)
  75. def format(self, error: Violation) -> str | None:
  76. """Format an error reported by Flake8.
  77. This method **must** be implemented by subclasses.
  78. :param error:
  79. This will be an instance of
  80. :class:`~flake8.violation.Violation`.
  81. :returns:
  82. The formatted error string.
  83. """
  84. raise NotImplementedError(
  85. "Subclass of BaseFormatter did not implement" " format."
  86. )
  87. def show_statistics(self, statistics: Statistics) -> None:
  88. """Format and print the statistics."""
  89. for error_code in statistics.error_codes():
  90. stats_for_error_code = statistics.statistics_for(error_code)
  91. statistic = next(stats_for_error_code)
  92. count = statistic.count
  93. count += sum(stat.count for stat in stats_for_error_code)
  94. self._write(f"{count:<5} {error_code} {statistic.message}")
  95. def show_benchmarks(self, benchmarks: list[tuple[str, float]]) -> None:
  96. """Format and print the benchmarks."""
  97. # NOTE(sigmavirus24): The format strings are a little confusing, even
  98. # to me, so here's a quick explanation:
  99. # We specify the named value first followed by a ':' to indicate we're
  100. # formatting the value.
  101. # Next we use '<' to indicate we want the value left aligned.
  102. # Then '10' is the width of the area.
  103. # For floats, finally, we only want only want at most 3 digits after
  104. # the decimal point to be displayed. This is the precision and it
  105. # can not be specified for integers which is why we need two separate
  106. # format strings.
  107. float_format = "{value:<10.3} {statistic}".format
  108. int_format = "{value:<10} {statistic}".format
  109. for statistic, value in benchmarks:
  110. if isinstance(value, int):
  111. benchmark = int_format(statistic=statistic, value=value)
  112. else:
  113. benchmark = float_format(statistic=statistic, value=value)
  114. self._write(benchmark)
  115. def show_source(self, error: Violation) -> str | None:
  116. """Show the physical line generating the error.
  117. This also adds an indicator for the particular part of the line that
  118. is reported as generating the problem.
  119. :param error:
  120. This will be an instance of
  121. :class:`~flake8.violation.Violation`.
  122. :returns:
  123. The formatted error string if the user wants to show the source.
  124. If the user does not want to show the source, this will return
  125. ``None``.
  126. """
  127. if not self.options.show_source or error.physical_line is None:
  128. return ""
  129. # Because column numbers are 1-indexed, we need to remove one to get
  130. # the proper number of space characters.
  131. indent = "".join(
  132. c if c.isspace() else " "
  133. for c in error.physical_line[: error.column_number - 1]
  134. )
  135. # Physical lines have a newline at the end, no need to add an extra
  136. # one
  137. return f"{error.physical_line}{indent}^"
  138. def _write(self, output: str) -> None:
  139. """Handle logic of whether to use an output file or print()."""
  140. if self.output_fd is not None:
  141. self.output_fd.write(output + self.newline)
  142. if self.output_fd is None or self.options.tee:
  143. sys.stdout.buffer.write(output.encode() + self.newline.encode())
  144. def write(self, line: str | None, source: str | None) -> None:
  145. """Write the line either to the output file or stdout.
  146. This handles deciding whether to write to a file or print to standard
  147. out for subclasses. Override this if you want behaviour that differs
  148. from the default.
  149. :param line:
  150. The formatted string to print or write.
  151. :param source:
  152. The source code that has been formatted and associated with the
  153. line of output.
  154. """
  155. if line:
  156. self._write(line)
  157. if source:
  158. self._write(source)
  159. def stop(self) -> None:
  160. """Clean up after reporting is finished."""
  161. if self.output_fd is not None:
  162. self.output_fd.close()
  163. self.output_fd = None