pyflakes.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. """Plugin built-in to Flake8 to treat pyflakes as a plugin."""
  2. from __future__ import annotations
  3. import argparse
  4. import ast
  5. import logging
  6. import os
  7. from typing import Any
  8. from typing import Generator
  9. import pyflakes.checker
  10. from flake8 import utils
  11. from flake8.options.manager import OptionManager
  12. LOG = logging.getLogger(__name__)
  13. FLAKE8_PYFLAKES_CODES = {
  14. "UnusedImport": "F401",
  15. "ImportShadowedByLoopVar": "F402",
  16. "ImportStarUsed": "F403",
  17. "LateFutureImport": "F404",
  18. "ImportStarUsage": "F405",
  19. "ImportStarNotPermitted": "F406",
  20. "FutureFeatureNotDefined": "F407",
  21. "PercentFormatInvalidFormat": "F501",
  22. "PercentFormatExpectedMapping": "F502",
  23. "PercentFormatExpectedSequence": "F503",
  24. "PercentFormatExtraNamedArguments": "F504",
  25. "PercentFormatMissingArgument": "F505",
  26. "PercentFormatMixedPositionalAndNamed": "F506",
  27. "PercentFormatPositionalCountMismatch": "F507",
  28. "PercentFormatStarRequiresSequence": "F508",
  29. "PercentFormatUnsupportedFormatCharacter": "F509",
  30. "StringDotFormatInvalidFormat": "F521",
  31. "StringDotFormatExtraNamedArguments": "F522",
  32. "StringDotFormatExtraPositionalArguments": "F523",
  33. "StringDotFormatMissingArgument": "F524",
  34. "StringDotFormatMixingAutomatic": "F525",
  35. "FStringMissingPlaceholders": "F541",
  36. "MultiValueRepeatedKeyLiteral": "F601",
  37. "MultiValueRepeatedKeyVariable": "F602",
  38. "TooManyExpressionsInStarredAssignment": "F621",
  39. "TwoStarredExpressions": "F622",
  40. "AssertTuple": "F631",
  41. "IsLiteral": "F632",
  42. "InvalidPrintSyntax": "F633",
  43. "IfTuple": "F634",
  44. "BreakOutsideLoop": "F701",
  45. "ContinueOutsideLoop": "F702",
  46. "YieldOutsideFunction": "F704",
  47. "ReturnOutsideFunction": "F706",
  48. "DefaultExceptNotLast": "F707",
  49. "DoctestSyntaxError": "F721",
  50. "ForwardAnnotationSyntaxError": "F722",
  51. "RedefinedWhileUnused": "F811",
  52. "UndefinedName": "F821",
  53. "UndefinedExport": "F822",
  54. "UndefinedLocal": "F823",
  55. "DuplicateArgument": "F831",
  56. "UnusedVariable": "F841",
  57. "UnusedAnnotation": "F842",
  58. "RaiseNotImplemented": "F901",
  59. }
  60. class FlakesChecker(pyflakes.checker.Checker):
  61. """Subclass the Pyflakes checker to conform with the flake8 API."""
  62. with_doctest = False
  63. include_in_doctest: list[str] = []
  64. exclude_from_doctest: list[str] = []
  65. def __init__(self, tree: ast.AST, filename: str) -> None:
  66. """Initialize the PyFlakes plugin with an AST tree and filename."""
  67. filename = utils.normalize_path(filename)
  68. with_doctest = self.with_doctest
  69. included_by = [
  70. include
  71. for include in self.include_in_doctest
  72. if include != "" and filename.startswith(include)
  73. ]
  74. if included_by:
  75. with_doctest = True
  76. for exclude in self.exclude_from_doctest:
  77. if exclude != "" and filename.startswith(exclude):
  78. with_doctest = False
  79. overlapped_by = [
  80. include
  81. for include in included_by
  82. if include.startswith(exclude)
  83. ]
  84. if overlapped_by:
  85. with_doctest = True
  86. super().__init__(tree, filename=filename, withDoctest=with_doctest)
  87. @classmethod
  88. def add_options(cls, parser: OptionManager) -> None:
  89. """Register options for PyFlakes on the Flake8 OptionManager."""
  90. parser.add_option(
  91. "--builtins",
  92. parse_from_config=True,
  93. comma_separated_list=True,
  94. help="define more built-ins, comma separated",
  95. )
  96. parser.add_option(
  97. "--doctests",
  98. default=False,
  99. action="store_true",
  100. parse_from_config=True,
  101. help="also check syntax of the doctests",
  102. )
  103. parser.add_option(
  104. "--include-in-doctest",
  105. default="",
  106. dest="include_in_doctest",
  107. parse_from_config=True,
  108. comma_separated_list=True,
  109. normalize_paths=True,
  110. help="Run doctests only on these files",
  111. )
  112. parser.add_option(
  113. "--exclude-from-doctest",
  114. default="",
  115. dest="exclude_from_doctest",
  116. parse_from_config=True,
  117. comma_separated_list=True,
  118. normalize_paths=True,
  119. help="Skip these files when running doctests",
  120. )
  121. @classmethod
  122. def parse_options(cls, options: argparse.Namespace) -> None:
  123. """Parse option values from Flake8's OptionManager."""
  124. if options.builtins:
  125. cls.builtIns = cls.builtIns.union(options.builtins)
  126. cls.with_doctest = options.doctests
  127. if options.include_in_doctest or options.exclude_from_doctest:
  128. LOG.warning(
  129. "--include-in-doctest / --exclude-from-doctest will be "
  130. "removed in a future version. see PyCQA/flake8#1747"
  131. )
  132. included_files = []
  133. for included_file in options.include_in_doctest:
  134. if included_file == "":
  135. continue
  136. if not included_file.startswith((os.sep, "./", "~/")):
  137. included_files.append(f"./{included_file}")
  138. else:
  139. included_files.append(included_file)
  140. cls.include_in_doctest = utils.normalize_paths(included_files)
  141. excluded_files = []
  142. for excluded_file in options.exclude_from_doctest:
  143. if excluded_file == "":
  144. continue
  145. if not excluded_file.startswith((os.sep, "./", "~/")):
  146. excluded_files.append(f"./{excluded_file}")
  147. else:
  148. excluded_files.append(excluded_file)
  149. cls.exclude_from_doctest = utils.normalize_paths(excluded_files)
  150. inc_exc = set(cls.include_in_doctest).intersection(
  151. cls.exclude_from_doctest
  152. )
  153. if inc_exc:
  154. raise ValueError(
  155. f"{inc_exc!r} was specified in both the "
  156. f"include-in-doctest and exclude-from-doctest "
  157. f"options. You are not allowed to specify it in "
  158. f"both for doctesting."
  159. )
  160. def run(self) -> Generator[tuple[int, int, str, type[Any]], None, None]:
  161. """Run the plugin."""
  162. for message in self.messages:
  163. col = getattr(message, "col", 0)
  164. yield (
  165. message.lineno,
  166. col,
  167. "{} {}".format(
  168. FLAKE8_PYFLAKES_CODES.get(type(message).__name__, "F999"),
  169. message.message % message.message_args,
  170. ),
  171. message.__class__,
  172. )