home *** CD-ROM | disk | FTP | other *** search
/ Freelog 33 / Freelog033.iso / Progr / Python-2.2.1.exe / TABNANNY.PY < prev    next >
Encoding:
Python Source  |  2001-08-07  |  10.5 KB  |  305 lines

  1. #! /usr/bin/env python
  2.  
  3. """The Tab Nanny despises ambiguous indentation.  She knows no mercy."""
  4.  
  5. # Released to the public domain, by Tim Peters, 15 April 1998.
  6.  
  7. # XXX Note: this is now a standard library module.
  8. # XXX The API needs to undergo changes however; the current code is too
  9. # XXX script-like.  This will be addressed later.
  10.  
  11. __version__ = "6"
  12.  
  13. import os
  14. import sys
  15. import getopt
  16. import tokenize
  17. if not hasattr(tokenize, 'NL'):
  18.     raise ValueError("tokenize.NL doesn't exist -- tokenize module too old")
  19.  
  20. __all__ = ["check", "NannyNag", "process_tokens"]
  21.  
  22. verbose = 0
  23. filename_only = 0
  24.  
  25. def errprint(*args):
  26.     sep = ""
  27.     for arg in args:
  28.         sys.stderr.write(sep + str(arg))
  29.         sep = " "
  30.     sys.stderr.write("\n")
  31.  
  32. def main():
  33.     global verbose, filename_only
  34.     try:
  35.         opts, args = getopt.getopt(sys.argv[1:], "qv")
  36.     except getopt.error, msg:
  37.         errprint(msg)
  38.         return
  39.     for o, a in opts:
  40.         if o == '-q':
  41.             filename_only = filename_only + 1
  42.         if o == '-v':
  43.             verbose = verbose + 1
  44.     if not args:
  45.         errprint("Usage:", sys.argv[0], "[-v] file_or_directory ...")
  46.         return
  47.     for arg in args:
  48.         check(arg)
  49.  
  50. class NannyNag:
  51.     def __init__(self, lineno, msg, line):
  52.         self.lineno, self.msg, self.line = lineno, msg, line
  53.     def get_lineno(self):
  54.         return self.lineno
  55.     def get_msg(self):
  56.         return self.msg
  57.     def get_line(self):
  58.         return self.line
  59.  
  60. def check(file):
  61.     if os.path.isdir(file) and not os.path.islink(file):
  62.         if verbose:
  63.             print "%s: listing directory" % `file`
  64.         names = os.listdir(file)
  65.         for name in names:
  66.             fullname = os.path.join(file, name)
  67.             if (os.path.isdir(fullname) and
  68.                 not os.path.islink(fullname) or
  69.                 os.path.normcase(name[-3:]) == ".py"):
  70.                 check(fullname)
  71.         return
  72.  
  73.     try:
  74.         f = open(file)
  75.     except IOError, msg:
  76.         errprint("%s: I/O Error: %s" % (`file`, str(msg)))
  77.         return
  78.  
  79.     if verbose > 1:
  80.         print "checking", `file`, "..."
  81.  
  82.     try:
  83.         process_tokens(tokenize.generate_tokens(f.readline))
  84.  
  85.     except tokenize.TokenError, msg:
  86.         errprint("%s: Token Error: %s" % (`file`, str(msg)))
  87.         return
  88.  
  89.     except NannyNag, nag:
  90.         badline = nag.get_lineno()
  91.         line = nag.get_line()
  92.         if verbose:
  93.             print "%s: *** Line %d: trouble in tab city! ***" % (
  94.                 `file`, badline)
  95.             print "offending line:", `line`
  96.             print nag.get_msg()
  97.         else:
  98.             if ' ' in file: file = '"' + file + '"'
  99.             if filename_only: print file
  100.             else: print file, badline, `line`
  101.         return
  102.  
  103.     if verbose:
  104.         print "%s: Clean bill of health." % `file`
  105.  
  106. class Whitespace:
  107.     # the characters used for space and tab
  108.     S, T = ' \t'
  109.  
  110.     # members:
  111.     #   raw
  112.     #       the original string
  113.     #   n
  114.     #       the number of leading whitespace characters in raw
  115.     #   nt
  116.     #       the number of tabs in raw[:n]
  117.     #   norm
  118.     #       the normal form as a pair (count, trailing), where:
  119.     #       count
  120.     #           a tuple such that raw[:n] contains count[i]
  121.     #           instances of S * i + T
  122.     #       trailing
  123.     #           the number of trailing spaces in raw[:n]
  124.     #       It's A Theorem that m.indent_level(t) ==
  125.     #       n.indent_level(t) for all t >= 1 iff m.norm == n.norm.
  126.     #   is_simple
  127.     #       true iff raw[:n] is of the form (T*)(S*)
  128.  
  129.     def __init__(self, ws):
  130.         self.raw  = ws
  131.         S, T = Whitespace.S, Whitespace.T
  132.         count = []
  133.         b = n = nt = 0
  134.         for ch in self.raw:
  135.             if ch == S:
  136.                 n = n + 1
  137.                 b = b + 1
  138.             elif ch == T:
  139.                 n = n + 1
  140.                 nt = nt + 1
  141.                 if b >= len(count):
  142.                     count = count + [0] * (b - len(count) + 1)
  143.                 count[b] = count[b] + 1
  144.                 b = 0
  145.             else:
  146.                 break
  147.         self.n    = n
  148.         self.nt   = nt
  149.         self.norm = tuple(count), b
  150.         self.is_simple = len(count) <= 1
  151.  
  152.     # return length of longest contiguous run of spaces (whether or not
  153.     # preceding a tab)
  154.     def longest_run_of_spaces(self):
  155.         count, trailing = self.norm
  156.         return max(len(count)-1, trailing)
  157.  
  158.     def indent_level(self, tabsize):
  159.         # count, il = self.norm
  160.         # for i in range(len(count)):
  161.         #    if count[i]:
  162.         #        il = il + (i/tabsize + 1)*tabsize * count[i]
  163.         # return il
  164.  
  165.         # quicker:
  166.         # il = trailing + sum (i/ts + 1)*ts*count[i] =
  167.         # trailing + ts * sum (i/ts + 1)*count[i] =
  168.         # trailing + ts * sum i/ts*count[i] + count[i] =
  169.         # trailing + ts * [(sum i/ts*count[i]) + (sum count[i])] =
  170.         # trailing + ts * [(sum i/ts*count[i]) + num_tabs]
  171.         # and note that i/ts*count[i] is 0 when i < ts
  172.  
  173.         count, trailing = self.norm
  174.         il = 0
  175.         for i in range(tabsize, len(count)):
  176.             il = il + i/tabsize * count[i]
  177.         return trailing + tabsize * (il + self.nt)
  178.  
  179.     # return true iff self.indent_level(t) == other.indent_level(t)
  180.     # for all t >= 1
  181.     def equal(self, other):
  182.         return self.norm == other.norm
  183.  
  184.     # return a list of tuples (ts, i1, i2) such that
  185.     # i1 == self.indent_level(ts) != other.indent_level(ts) == i2.
  186.     # Intended to be used after not self.equal(other) is known, in which
  187.     # case it will return at least one witnessing tab size.
  188.     def not_equal_witness(self, other):
  189.         n = max(self.longest_run_of_spaces(),
  190.                 other.longest_run_of_spaces()) + 1
  191.         a = []
  192.         for ts in range(1, n+1):
  193.             if self.indent_level(ts) != other.indent_level(ts):
  194.                 a.append( (ts,
  195.                            self.indent_level(ts),
  196.                            other.indent_level(ts)) )
  197.         return a
  198.  
  199.     # Return true iff self.indent_level(t) < other.indent_level(t)
  200.     # for all t >= 1.
  201.     # The algorithm is due to Vincent Broman.
  202.     # Easy to prove it's correct.
  203.     # XXXpost that.
  204.     # Trivial to prove n is sharp (consider T vs ST).
  205.     # Unknown whether there's a faster general way.  I suspected so at
  206.     # first, but no longer.
  207.     # For the special (but common!) case where M and N are both of the
  208.     # form (T*)(S*), M.less(N) iff M.len() < N.len() and
  209.     # M.num_tabs() <= N.num_tabs(). Proof is easy but kinda long-winded.
  210.     # XXXwrite that up.
  211.     # Note that M is of the form (T*)(S*) iff len(M.norm[0]) <= 1.
  212.     def less(self, other):
  213.         if self.n >= other.n:
  214.             return 0
  215.         if self.is_simple and other.is_simple:
  216.             return self.nt <= other.nt
  217.         n = max(self.longest_run_of_spaces(),
  218.                 other.longest_run_of_spaces()) + 1
  219.         # the self.n >= other.n test already did it for ts=1
  220.         for ts in range(2, n+1):
  221.             if self.indent_level(ts) >= other.indent_level(ts):
  222.                 return 0
  223.         return 1
  224.  
  225.     # return a list of tuples (ts, i1, i2) such that
  226.     # i1 == self.indent_level(ts) >= other.indent_level(ts) == i2.
  227.     # Intended to be used after not self.less(other) is known, in which
  228.     # case it will return at least one witnessing tab size.
  229.     def not_less_witness(self, other):
  230.         n = max(self.longest_run_of_spaces(),
  231.                 other.longest_run_of_spaces()) + 1
  232.         a = []
  233.         for ts in range(1, n+1):
  234.             if self.indent_level(ts) >= other.indent_level(ts):
  235.                 a.append( (ts,
  236.                            self.indent_level(ts),
  237.                            other.indent_level(ts)) )
  238.         return a
  239.  
  240. def format_witnesses(w):
  241.     import string
  242.     firsts = map(lambda tup: str(tup[0]), w)
  243.     prefix = "at tab size"
  244.     if len(w) > 1:
  245.         prefix = prefix + "s"
  246.     return prefix + " " + string.join(firsts, ', ')
  247.  
  248. def process_tokens(tokens):
  249.     INDENT = tokenize.INDENT
  250.     DEDENT = tokenize.DEDENT
  251.     NEWLINE = tokenize.NEWLINE
  252.     JUNK = tokenize.COMMENT, tokenize.NL
  253.     indents = [Whitespace("")]
  254.     check_equal = 0
  255.  
  256.     for (type, token, start, end, line) in tokens:
  257.         if type == NEWLINE:
  258.             # a program statement, or ENDMARKER, will eventually follow,
  259.             # after some (possibly empty) run of tokens of the form
  260.             #     (NL | COMMENT)* (INDENT | DEDENT+)?
  261.             # If an INDENT appears, setting check_equal is wrong, and will
  262.             # be undone when we see the INDENT.
  263.             check_equal = 1
  264.  
  265.         elif type == INDENT:
  266.             check_equal = 0
  267.             thisguy = Whitespace(token)
  268.             if not indents[-1].less(thisguy):
  269.                 witness = indents[-1].not_less_witness(thisguy)
  270.                 msg = "indent not greater e.g. " + format_witnesses(witness)
  271.                 raise NannyNag(start[0], msg, line)
  272.             indents.append(thisguy)
  273.  
  274.         elif type == DEDENT:
  275.             # there's nothing we need to check here!  what's important is
  276.             # that when the run of DEDENTs ends, the indentation of the
  277.             # program statement (or ENDMARKER) that triggered the run is
  278.             # equal to what's left at the top of the indents stack
  279.  
  280.             # Ouch!  This assert triggers if the last line of the source
  281.             # is indented *and* lacks a newline -- then DEDENTs pop out
  282.             # of thin air.
  283.             # assert check_equal  # else no earlier NEWLINE, or an earlier INDENT
  284.             check_equal = 1
  285.  
  286.             del indents[-1]
  287.  
  288.         elif check_equal and type not in JUNK:
  289.             # this is the first "real token" following a NEWLINE, so it
  290.             # must be the first token of the next program statement, or an
  291.             # ENDMARKER; the "line" argument exposes the leading whitespace
  292.             # for this statement; in the case of ENDMARKER, line is an empty
  293.             # string, so will properly match the empty string with which the
  294.             # "indents" stack was seeded
  295.             check_equal = 0
  296.             thisguy = Whitespace(line)
  297.             if not indents[-1].equal(thisguy):
  298.                 witness = indents[-1].not_equal_witness(thisguy)
  299.                 msg = "indent not equal e.g. " + format_witnesses(witness)
  300.                 raise NannyNag(start[0], msg, line)
  301.  
  302.  
  303. if __name__ == '__main__':
  304.     main()
  305.