api.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  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. w = checker.Checker(tree, filename=filename)
  39. w.messages.sort(key=lambda m: m.lineno)
  40. for warning in w.messages:
  41. reporter.flake(warning)
  42. return len(w.messages)
  43. def checkPath(filename, reporter=None):
  44. """
  45. Check the given path, printing out any warnings detected.
  46. @param reporter: A L{Reporter} instance, where errors and warnings will be
  47. reported.
  48. @return: the number of warnings printed
  49. """
  50. if reporter is None:
  51. reporter = modReporter._makeDefaultReporter()
  52. try:
  53. with open(filename, 'rb') as f:
  54. codestr = f.read()
  55. except OSError as e:
  56. reporter.unexpectedError(filename, e.args[1])
  57. return 1
  58. return check(codestr, filename, reporter)
  59. def isPythonFile(filename):
  60. """Return True if filename points to a Python file."""
  61. if filename.endswith('.py'):
  62. return True
  63. # Avoid obvious Emacs backup files
  64. if filename.endswith("~"):
  65. return False
  66. max_bytes = 128
  67. try:
  68. with open(filename, 'rb') as f:
  69. text = f.read(max_bytes)
  70. if not text:
  71. return False
  72. except OSError:
  73. return False
  74. return PYTHON_SHEBANG_REGEX.match(text)
  75. def iterSourceCode(paths):
  76. """
  77. Iterate over all Python source files in C{paths}.
  78. @param paths: A list of paths. Directories will be recursed into and
  79. any .py files found will be yielded. Any non-directories will be
  80. yielded as-is.
  81. """
  82. for path in paths:
  83. if os.path.isdir(path):
  84. for dirpath, dirnames, filenames in os.walk(path):
  85. for filename in filenames:
  86. full_path = os.path.join(dirpath, filename)
  87. if isPythonFile(full_path):
  88. yield full_path
  89. else:
  90. yield path
  91. def checkRecursive(paths, reporter):
  92. """
  93. Recursively check all source files in C{paths}.
  94. @param paths: A list of paths to Python source files and directories
  95. containing Python source files.
  96. @param reporter: A L{Reporter} where all of the warnings and errors
  97. will be reported to.
  98. @return: The number of warnings found.
  99. """
  100. warnings = 0
  101. for sourcePath in iterSourceCode(paths):
  102. warnings += checkPath(sourcePath, reporter)
  103. return warnings
  104. def _exitOnSignal(sigName, message):
  105. """Handles a signal with sys.exit.
  106. Some of these signals (SIGPIPE, for example) don't exist or are invalid on
  107. Windows. So, ignore errors that might arise.
  108. """
  109. import signal
  110. try:
  111. sigNumber = getattr(signal, sigName)
  112. except AttributeError:
  113. # the signal constants defined in the signal module are defined by
  114. # whether the C library supports them or not. So, SIGPIPE might not
  115. # even be defined.
  116. return
  117. def handler(sig, f):
  118. sys.exit(message)
  119. try:
  120. signal.signal(sigNumber, handler)
  121. except ValueError:
  122. # It's also possible the signal is defined, but then it's invalid. In
  123. # this case, signal.signal raises ValueError.
  124. pass
  125. def _get_version():
  126. """
  127. Retrieve and format package version along with python version & OS used
  128. """
  129. return ('%s Python %s on %s' %
  130. (__version__, platform.python_version(), platform.system()))
  131. def main(prog=None, args=None):
  132. """Entry point for the script "pyflakes"."""
  133. import argparse
  134. # Handle "Keyboard Interrupt" and "Broken pipe" gracefully
  135. _exitOnSignal('SIGINT', '... stopped')
  136. _exitOnSignal('SIGPIPE', 1)
  137. parser = argparse.ArgumentParser(prog=prog,
  138. description='Check Python source files for errors')
  139. parser.add_argument('-V', '--version', action='version', version=_get_version())
  140. parser.add_argument('path', nargs='*',
  141. help='Path(s) of Python file(s) to check. STDIN if not given.')
  142. args = parser.parse_args(args=args).path
  143. reporter = modReporter._makeDefaultReporter()
  144. if args:
  145. warnings = checkRecursive(args, reporter)
  146. else:
  147. warnings = check(sys.stdin.read(), '<stdin>', reporter)
  148. raise SystemExit(warnings > 0)