flakeplus.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. #!/usr/bin/env python
  2. from __future__ import absolute_import
  3. from __future__ import with_statement
  4. import os
  5. import pprint
  6. import re
  7. import sys
  8. from collections import defaultdict
  9. from itertools import starmap
  10. from unipath import Path
  11. RE_COMMENT = r'^\s*\#'
  12. RE_NOQA = r'.+?\#\s+noqa+'
  13. RE_MULTILINE_COMMENT_O = r'^\s*(?:\'\'\'|""").+?(?:\'\'\'|""")'
  14. RE_MULTILINE_COMMENT_S = r'^\s*(?:\'\'\'|""")'
  15. RE_MULTILINE_COMMENT_E = r'(?:^|.+?)(?:\'\'\'|""")'
  16. RE_WITH = r'(?:^|\s+)with\s+'
  17. RE_WITH_IMPORT = r'''from\s+ __future__\s+ import\s+ with_statement'''
  18. RE_PRINT = r'''(?:^|\s+)print\((?:"|')\W+?[A-Z0-9:]{2,}'''
  19. RE_ABS_IMPORT = r'''from\s+ __future__\s+ import\s+ absolute_import'''
  20. acc = defaultdict(lambda: {"abs": False, "print": False})
  21. def compile(regex):
  22. return re.compile(regex, re.VERBOSE)
  23. class FlakePP(object):
  24. re_comment = compile(RE_COMMENT)
  25. re_ml_comment_o = compile(RE_MULTILINE_COMMENT_O)
  26. re_ml_comment_s = compile(RE_MULTILINE_COMMENT_S)
  27. re_ml_comment_e = compile(RE_MULTILINE_COMMENT_E)
  28. re_abs_import = compile(RE_ABS_IMPORT)
  29. re_print = compile(RE_PRINT)
  30. re_with_import = compile(RE_WITH_IMPORT)
  31. re_with = compile(RE_WITH)
  32. re_noqa = compile(RE_NOQA)
  33. map = {"abs": False, "print": False,
  34. "with": False, "with-used": False}
  35. def __init__(self, verbose=False):
  36. self.verbose = verbose
  37. self.steps = (("abs", self.re_abs_import),
  38. ("with", self.re_with_import),
  39. ("with-used", self.re_with),
  40. ("print", self.re_print))
  41. def analyze_fh(self, fh):
  42. steps = self.steps
  43. filename = fh.name
  44. acc = dict(self.map)
  45. index = 0
  46. errors = [0]
  47. def error(fmt, **kwargs):
  48. errors[0] += 1
  49. self.announce(fmt, **dict(kwargs, filename=filename))
  50. for index, line in enumerate(self.strip_comments(fh)):
  51. for key, pattern in self.steps:
  52. if pattern.match(line):
  53. acc[key] = True
  54. if index:
  55. if not acc["abs"]:
  56. error("%(filename)s: missing abs import")
  57. if acc["with-used"] and not acc["with"]:
  58. error("%(filename)s: missing with import")
  59. if acc["print"]:
  60. error("%(filename)s: left over print statement")
  61. return filename, errors[0], acc
  62. def analyze_file(self, filename):
  63. with open(filename) as fh:
  64. return self.analyze_fh(fh)
  65. def analyze_tree(self, dir):
  66. for dirpath, _, filenames in os.walk(dir):
  67. for path in (Path(dirpath, f) for f in filenames):
  68. if path.endswith(".py"):
  69. yield self.analyze_file(path)
  70. def analyze(self, *paths):
  71. for path in map(Path, paths):
  72. if path.isdir():
  73. for res in self.analyze_tree(path):
  74. yield res
  75. else:
  76. yield self.analyze_file(path)
  77. def strip_comments(self, fh):
  78. re_comment = self.re_comment
  79. re_ml_comment_o = self.re_ml_comment_o
  80. re_ml_comment_s = self.re_ml_comment_s
  81. re_ml_comment_e = self.re_ml_comment_e
  82. re_noqa = self.re_noqa
  83. in_ml = False
  84. for line in fh.readlines():
  85. if in_ml:
  86. if re_ml_comment_e.match(line):
  87. in_ml = False
  88. else:
  89. if re_noqa.match(line) or re_ml_comment_o.match(line):
  90. pass
  91. elif re_ml_comment_s.match(line):
  92. in_ml = True
  93. elif re_comment.match(line):
  94. pass
  95. else:
  96. yield line
  97. def announce(self, fmt, **kwargs):
  98. sys.stderr.write((fmt + "\n") % kwargs)
  99. def main(argv=sys.argv, exitcode=0):
  100. for _, errors, _ in FlakePP(verbose=True).analyze(*argv[1:]):
  101. if errors:
  102. exitcode = 1
  103. return exitcode
  104. if __name__ == "__main__":
  105. sys.exit(main())