# Python 2.7, 794

<!-- language-all: lang-python -->

I have been meaning to build a minifier for Python for a while, so this is a good opportunity to investigate the problem.

The program uses a mix of regular expression analysis, and Python parser operations. White space is minimised. Variable that are defined by the user are replaced by a single letter variable (which is not in use!). Finally the `while True` statement is put on a diet.

The three test cases all verify as running correctly. I could imagine some pathological examples which could result in errors in the generated code but the algorithm should be robust under most circumstances.

## Results

    228 t1.py
    128 t2.py
    438 t3.py
    794 total

## Code

    import sys
    import re
    from tokenize import generate_tokens
    from token import tok_name
    from keyword import iskeyword

    wr = sys.stdout.write

    def pyparse(text):
        'Return [TYPE,TOKEN] pair list'
        # Use KEYWORD,NAME,NUMBER,OP,STRING,NL,NEWLINE,COMMENT,INDENT,DEDENT
        rawtokens = generate_tokens(text.readline)
        tokens = [[tok_name[n], t] for n,t,p1,p2,dx in rawtokens]
        for tpair in tokens:
            if tpair[0] == 'NAME' and iskeyword(tpair[1]):
                tpair[0] = 'KEYWORD'
        return tokens

    def finduservars(filename):
        'Return a set of user variables that we can replace with a-zA-Z'
        varset = set()
        for line in open(filename):
            line = line.strip()
            match = re.match(r'def\s+(\w+)\s*\((.*)\)\s*:', line)
            if match:
                func, args = match.groups()
                varset.add(func)
                arglist = re.findall(r'(\w+|=)', args)
                for a in arglist:
                    if a == '=':
                        break  # keyword args follow - too hard to parse
                    varset.add(a)
                continue
            match = re.match(r'(\w+)\s*=.+', line)
            if match:
                assigned = match.group(1)
                varset.add(assigned)
                continue
        return set(v for v in list(varset) if len(v) > 1)

    filename = sys.argv[1]
    tokenlist = pyparse(open(filename))

    # Build map for var->char conversion:
    varset = finduservars(filename)
    singles = [text for tok,text in tokenlist if tok=='NAME' and len(text)==1]
    allvar = [chr(n) for n in range(97,123)+range(65,91)]
    charvar = [c for c in allvar if c not in singles]
    varreplaced = list(varset)[:len(charvar)]
    varmap = dict((v, charvar.pop(0)) for v in varreplaced)

    prev = 'NONE'
    indent = ['']
    output = []
    add = output.append
    for tok, text in tokenlist:
        if tok == 'NL':
            continue
        elif tok == 'INDENT':
            indent.append( text.replace('    ', ' ') )
            output[-1] = indent[-1]
        elif tok == 'DEDENT':
            indent.pop(-1)
            output[-1] = indent[-1]
        elif tok == 'NEWLINE':
            add(text)
            add(indent[-1])
        elif tok in 'KEYWORD,NAME,NUMBER':
            if prev in 'KEYWORD,NAME,NUMBER':
                add(' ')
            if tok == 'NAME':
                if output[-2] == 'while' and text == 'True':
                    add('1') # common verbose idiom
                else:
                    add(varmap.get(text, text))
            else:
                add(text)
        else:
            add(text)
        prev = tok

    wr(''.join(output))