api.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. """
  2. API for the command-line I{pyflakes} tool.
  3. """
  4. import ast
  5. import os
  6. import platform
  7. import re
  8. import sys
  9. from pyflakes import checker, __version__
  10. from pyflakes import reporter as modReporter
  11. __all__ = ['check', 'checkPath', 'checkRecursive', 'iterSourceCode', 'main']
  12. PYTHON_SHEBANG_REGEX = re.compile(br'^#!.*\bpython(3(\.\d+)?|w)?[dmu]?\s')
  13. def check(codeString, filename, reporter=None):
  14. """
  15. Check the Python source given by C{codeString} for flakes.
  16. @param codeString: The Python source to check.
  17. @type codeString: C{str}
  18. @param filename: The name of the file the source came from, used to report
  19. errors.
  20. @type filename: C{str}
  21. @param reporter: A L{Reporter} instance, where errors and warnings will be
  22. reported.
  23. @return: The number of warnings emitted.
  24. @rtype: C{int}
  25. """
  26. if reporter is None:
  27. reporter = modReporter._makeDefaultReporter()
  28. # First, compile into an AST and handle syntax errors.
  29. try:
  30. tree = ast.parse(codeString, filename=filename)
  31. except SyntaxError as e:
  32. reporter.syntaxError(filename, e.args[0], e.lineno, e.offset, e.text)
  33. return 1
  34. except Exception:
  35. reporter.unexpectedError(filename, 'problem decoding source')
  36. return 1
  37. # Okay, it's syntactically valid. Now check it.
  38. file_tokens = checker.make_tokens(codeString)
  39. w = checker.Checker(tree, file_tokens=file_tokens, filename=filename)
  40. w.messages.sort(key=lambda m: m.lineno)
  41. for warning in w.messages:
  42. reporter.flake(warning)
  43. return len(w.messages)
  44. def checkPath(filename, reporter=None):
  45. """
  46. Check the given path, printing out any warnings detected.
  47. @param reporter: A L{Reporter} instance, where errors and warnings will be
  48. reported.
  49. @return: the number of warnings printed
  50. """
  51. if reporter is None:
  52. reporter = modReporter._makeDefaultReporter()
  53. try:
  54. with open(filename, 'rb') as f:
  55. codestr = f.read()
  56. except OSError as e:
  57. reporter.unexpectedError(filename, e.args[1])
  58. return 1
  59. return check(codestr, filename, reporter)
  60. def isPythonFile(filename):
  61. """Return True if filename points to a Python file."""
  62. if filename.endswith('.py'):
  63. return True
  64. # Avoid obvious Emacs backup files
  65. if filename.endswith("~"):
  66. return False
  67. max_bytes = 128
  68. try:
  69. with open(filename, 'rb') as f:
  70. text = f.read(max_bytes)
  71. if not text:
  72. return False
  73. except OSError:
  74. return False
  75. return PYTHON_SHEBANG_REGEX.match(text)
  76. def iterSourceCode(paths):
  77. """
  78. Iterate over all Python source files in C{paths}.
  79. @param paths: A list of paths. Directories will be recursed into and
  80. any .py files found will be yielded. Any non-directories will be
  81. yielded as-is.
  82. """
  83. for path in paths:
  84. if os.path.isdir(path):
  85. for dirpath, dirnames, filenames in os.walk(path):
  86. for filename in filenames:
  87. full_path = os.path.join(dirpath, filename)
  88. if isPythonFile(full_path):
  89. yield full_path
  90. else:
  91. yield path
  92. def checkRecursive(paths, reporter):
  93. """
  94. Recursively check all source files in C{paths}.
  95. @param paths: A list of paths to Python source files and directories
  96. containing Python source files.
  97. @param reporter: A L{Reporter} where all of the warnings and errors
  98. will be reported to.
  99. @return: The number of warnings found.
  100. """
  101. warnings = 0
  102. for sourcePath in iterSourceCode(paths):
  103. warnings += checkPath(sourcePath, reporter)
  104. return warnings
  105. def _exitOnSignal(sigName, message):
  106. """Handles a signal with sys.exit.
  107. Some of these signals (SIGPIPE, for example) don't exist or are invalid on
  108. Windows. So, ignore errors that might arise.
  109. """
  110. import signal
  111. try:
  112. sigNumber = getattr(signal, sigName)
  113. except AttributeError:
  114. # the signal constants defined in the signal module are defined by
  115. # whether the C library supports them or not. So, SIGPIPE might not
  116. # even be defined.
  117. return
  118. def handler(sig, f):
  119. sys.exit(message)
  120. try:
  121. signal.signal(sigNumber, handler)
  122. except ValueError:
  123. # It's also possible the signal is defined, but then it's invalid. In
  124. # this case, signal.signal raises ValueError.
  125. pass
  126. def _get_version():
  127. """
  128. Retrieve and format package version along with python version & OS used
  129. """
  130. return ('%s Python %s on %s' %
  131. (__version__, platform.python_version(), platform.system()))
  132. def main(prog=None, args=None):
  133. """Entry point for the script "pyflakes"."""
  134. import argparse
  135. # Handle "Keyboard Interrupt" and "Broken pipe" gracefully
  136. _exitOnSignal('SIGINT', '... stopped')
  137. _exitOnSignal('SIGPIPE', 1)
  138. parser = argparse.ArgumentParser(prog=prog,
  139. description='Check Python source files for errors')
  140. parser.add_argument('-V', '--version', action='version', version=_get_version())
  141. parser.add_argument('path', nargs='*',
  142. help='Path(s) of Python file(s) to check. STDIN if not given.')
  143. args = parser.parse_args(args=args).path
  144. reporter = modReporter._makeDefaultReporter()
  145. if args:
  146. warnings = checkRecursive(args, reporter)
  147. else:
  148. warnings = check(sys.stdin.read(), '<stdin>', reporter)
  149. raise SystemExit(warnings > 0)