statistics.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. """Statistic collection logic for Flake8."""
  2. from __future__ import annotations
  3. from typing import Generator
  4. from typing import NamedTuple
  5. from flake8.violation import Violation
  6. class Statistics:
  7. """Manager of aggregated statistics for a run of Flake8."""
  8. def __init__(self) -> None:
  9. """Initialize the underlying dictionary for our statistics."""
  10. self._store: dict[Key, Statistic] = {}
  11. def error_codes(self) -> list[str]:
  12. """Return all unique error codes stored.
  13. :returns:
  14. Sorted list of error codes.
  15. """
  16. return sorted({key.code for key in self._store})
  17. def record(self, error: Violation) -> None:
  18. """Add the fact that the error was seen in the file.
  19. :param error:
  20. The Violation instance containing the information about the
  21. violation.
  22. """
  23. key = Key.create_from(error)
  24. if key not in self._store:
  25. self._store[key] = Statistic.create_from(error)
  26. self._store[key].increment()
  27. def statistics_for(
  28. self, prefix: str, filename: str | None = None
  29. ) -> Generator[Statistic, None, None]:
  30. """Generate statistics for the prefix and filename.
  31. If you have a :class:`Statistics` object that has recorded errors,
  32. you can generate the statistics for a prefix (e.g., ``E``, ``E1``,
  33. ``W50``, ``W503``) with the optional filter of a filename as well.
  34. .. code-block:: python
  35. >>> stats = Statistics()
  36. >>> stats.statistics_for('E12',
  37. filename='src/flake8/statistics.py')
  38. <generator ...>
  39. >>> stats.statistics_for('W')
  40. <generator ...>
  41. :param prefix:
  42. The error class or specific error code to find statistics for.
  43. :param filename:
  44. (Optional) The filename to further filter results by.
  45. :returns:
  46. Generator of instances of :class:`Statistic`
  47. """
  48. matching_errors = sorted(
  49. key for key in self._store if key.matches(prefix, filename)
  50. )
  51. for error_code in matching_errors:
  52. yield self._store[error_code]
  53. class Key(NamedTuple):
  54. """Simple key structure for the Statistics dictionary.
  55. To make things clearer, easier to read, and more understandable, we use a
  56. namedtuple here for all Keys in the underlying dictionary for the
  57. Statistics object.
  58. """
  59. filename: str
  60. code: str
  61. @classmethod
  62. def create_from(cls, error: Violation) -> Key:
  63. """Create a Key from :class:`flake8.violation.Violation`."""
  64. return cls(filename=error.filename, code=error.code)
  65. def matches(self, prefix: str, filename: str | None) -> bool:
  66. """Determine if this key matches some constraints.
  67. :param prefix:
  68. The error code prefix that this key's error code should start with.
  69. :param filename:
  70. The filename that we potentially want to match on. This can be
  71. None to only match on error prefix.
  72. :returns:
  73. True if the Key's code starts with the prefix and either filename
  74. is None, or the Key's filename matches the value passed in.
  75. """
  76. return self.code.startswith(prefix) and (
  77. filename is None or self.filename == filename
  78. )
  79. class Statistic:
  80. """Simple wrapper around the logic of each statistic.
  81. Instead of maintaining a simple but potentially hard to reason about
  82. tuple, we create a class which has attributes and a couple
  83. convenience methods on it.
  84. """
  85. def __init__(
  86. self, error_code: str, filename: str, message: str, count: int
  87. ) -> None:
  88. """Initialize our Statistic."""
  89. self.error_code = error_code
  90. self.filename = filename
  91. self.message = message
  92. self.count = count
  93. @classmethod
  94. def create_from(cls, error: Violation) -> Statistic:
  95. """Create a Statistic from a :class:`flake8.violation.Violation`."""
  96. return cls(
  97. error_code=error.code,
  98. filename=error.filename,
  99. message=error.text,
  100. count=0,
  101. )
  102. def increment(self) -> None:
  103. """Increment the number of times we've seen this error in this file."""
  104. self.count += 1