tkunzip.py   [plain text]


"""Post-install GUI to compile to pyc and unpack twisted doco"""

from __future__ import generators

import sys
import zipfile
import py_compile

# we're going to ignore failures to import tkinter and fall back
# to using the console if the required dll is not found

# Scary kludge to work around tk84.dll bug: 
# https://sourceforge.net/tracker/index.php?func=detail&aid=814654&group_id=5470&atid=105470
# Without which(): you get a windows missing-dll popup message
from twisted.runner.procutils import which
if sys.version_info[:2]==(2,2):
    tkdll='tk83.dll'
else:
    tkdll='tk84.dll'
if which(tkdll) or which('DLLs/%s' % tkdll):
    try:
        import Tkinter
        from Tkinter import *
        from twisted.internet import tksupport
    except ImportError:
        pass

# twisted
from twisted.internet import reactor, defer
from twisted.python import failure, log, zipstream, util, usage, log
# local
import os.path

class ProgressBar:
    def __init__(self, master=None, orientation="horizontal",
                 min=0, max=100, width=100, height=18,
                 doLabel=1, appearance="sunken",
                 fillColor="blue", background="gray",
                 labelColor="yellow", labelFont="Arial",
                 labelText="", labelFormat="%d%%",
                 value=0, bd=2):
        # preserve various values
        self.master=master
        self.orientation=orientation
        self.min=min
        self.max=max
        self.width=width
        self.height=height
        self.doLabel=doLabel
        self.fillColor=fillColor
        self.labelFont= labelFont
        self.labelColor=labelColor
        self.background=background
        self.labelText=labelText
        self.labelFormat=labelFormat
        self.value=value
        self.frame=Frame(master, relief=appearance, bd=bd)
        self.canvas=Canvas(self.frame, height=height, width=width, bd=0,
                           highlightthickness=0, background=background)
        self.scale=self.canvas.create_rectangle(0, 0, width, height,
                                                fill=fillColor)
        self.label=self.canvas.create_text(self.canvas.winfo_reqwidth() / 2,
                                           height / 2, text=labelText,
                                           anchor="c", fill=labelColor,
                                           font=self.labelFont)
        self.update()
        self.canvas.pack(side='top', fill='x', expand='no')

    def pack(self, *args, **kwargs):
        self.frame.pack(*args, **kwargs)
    
    def updateProgress(self, newValue, newMax=None):
        if newMax:
            self.max = newMax
        self.value = newValue
        self.update()

    def update(self):
        # Trim the values to be between min and max
        value=self.value
        if value > self.max:
            value = self.max
        if value < self.min:
            value = self.min
        # Adjust the rectangle
        if self.orientation == "horizontal":
            self.canvas.coords(self.scale, 0, 0,
              float(value) / self.max * self.width, self.height)
        else:
            self.canvas.coords(self.scale, 0,
                               self.height - (float(value) / 
                                              self.max*self.height),
                               self.width, self.height)
        # Now update the colors
        self.canvas.itemconfig(self.scale, fill=self.fillColor)
        self.canvas.itemconfig(self.label, fill=self.labelColor)
        # And update the label
        if self.doLabel:
            if value:
                if value >= 0:
                    pvalue = int((float(value) / float(self.max)) * 
                                   100.0)
                else:
                    pvalue = 0
                self.canvas.itemconfig(self.label, text=self.labelFormat
                                         % pvalue)
            else:
                self.canvas.itemconfig(self.label, text='')
        else:
            self.canvas.itemconfig(self.label, text=self.labelFormat %
                                   self.labelText)
        self.canvas.update_idletasks()


class Progressor:
    """A base class to make it simple to hook a progress bar up to a process.
    """
    def __init__(self, title, *args, **kwargs):
        self.title=title
        self.stopping=0
        self.bar=None
        self.iterator=None
        self.remaining=1000

    def setBar(self, bar, max):
        self.bar=bar
        bar.updateProgress(0, max)
        return self

    def setIterator(self, iterator):
        self.iterator=iterator
        return self
    
    def updateBar(self, deferred):
        b=self.bar
        try:
            b.updateProgress(b.max - self.remaining)
        except TclError:
            self.stopping=1
        except:
            deferred.errback(failure.Failure())

    def processAll(self, root):
        assert self.bar and self.iterator, "must setBar and setIterator"
        self.root=root
        root.title(self.title)
        d=defer.Deferred()
        d.addErrback(log.err)
        reactor.callLater(0.1, self.processOne, d)
        return d

    def processOne(self, deferred):
        if self.stopping:
            deferred.callback(self.root)
            return
        
        try:
            self.remaining=self.iterator.next()
        except StopIteration:
            self.stopping=1            
        except:
            deferred.errback(failure.Failure())
        
        if self.remaining%10==0:
            reactor.callLater(0, self.updateBar, deferred)
        if self.remaining%100==0:
            log.msg(self.remaining)
        reactor.callLater(0, self.processOne, deferred)

def compiler(path):
    """A generator for compiling files to .pyc"""
    def justlist(arg, directory, names):
        pynames=[os.path.join(directory, n) for n in names
                 if n.endswith('.py')]
        arg.extend(pynames)
    all=[]
    os.path.walk(path, justlist, all)

    remaining=len(all)
    i=zip(all, range(remaining-1, -1, -1))
    for f, remaining in i:
        py_compile.compile(f)
        yield remaining

class TkunzipOptions(usage.Options):
    optParameters=[["zipfile", "z", "", "a zipfile"],
                   ["ziptargetdir", "t", ".", "where to extract zipfile"],
                   ["compiledir", "c", "", "a directory to compile"],
                   ]
    optFlags=[["use-console", "C", "show in the console, not graphically"],
              ["shell-exec", "x", """\
spawn a new console to show output (implies -C)"""],
              ]

def countPys(countl, directory, names):
    sofar=countl[0]
    sofar=sofar+len([f for f in names if f.endswith('.py')])
    countl[0]=sofar
    return sofar

def countPysRecursive(path):
    countl=[0]
    os.path.walk(path, countPys, countl)
    return countl[0]

def run(argv=sys.argv):
    log.startLogging(file('tkunzip.log', 'w'))
    opt=TkunzipOptions()
    try:
        opt.parseOptions(argv[1:])
    except usage.UsageError, e:
        print str(opt)
        print str(e)
        sys.exit(1)

    if opt['use-console']:
        # this should come before shell-exec to prevent infinite loop
        return doItConsolicious(opt)              
    if opt['shell-exec'] or not 'Tkinter' in sys.modules:
        from distutils import sysconfig
        from twisted.scripts import tkunzip
        myfile=tkunzip.__file__
        exe=os.path.join(sysconfig.get_config_var('prefix'), 'python.exe')
        return os.system('%s %s --use-console %s' % (exe, myfile,
                                                     ' '.join(argv[1:])))
    return doItTkinterly(opt)

def doItConsolicious(opt):
    # reclaim stdout/stderr from log
    sys.stdout = sys.__stdout__
    sys.stderr = sys.__stderr__
    if opt['zipfile']:
        print 'Unpacking documentation...'
        for n in zipstream.unzipIter(opt['zipfile'], opt['ziptargetdir']):
            if n % 100 == 0:
                print n,
            if n % 1000 == 0:
                print
        print 'Done unpacking.'
        
    if opt['compiledir']:
        print 'Compiling to pyc...'
        import compileall
        compileall.compile_dir(opt["compiledir"])
        print 'Done compiling.'

def doItTkinterly(opt):
    root=Tkinter.Tk()
    root.withdraw()
    root.title('One Moment.')
    root.protocol('WM_DELETE_WINDOW', reactor.stop)
    tksupport.install(root)
    
    prog=ProgressBar(root, value=0, labelColor="black", width=200)
    prog.pack()

    # callback immediately
    d=defer.succeed(root).addErrback(log.err)

    def deiconify(root):
        root.deiconify()
        return root

    d.addCallback(deiconify)
    
    if opt['zipfile']:
        uz=Progressor('Unpacking documentation...')
        max=zipstream.countZipFileChunks(opt['zipfile'], 4096)
        uz.setBar(prog, max)
        uz.setIterator(zipstream.unzipIterChunky(opt['zipfile'],
                                                 opt['ziptargetdir']))
        d.addCallback(uz.processAll)

    if opt['compiledir']:
        comp=Progressor('Compiling to pyc...')
        comp.setBar(prog, countPysRecursive(opt['compiledir']))
        comp.setIterator(compiler(opt['compiledir']))
        d.addCallback(comp.processAll)

    def stop(ignore):
        reactor.stop()
        root.destroy()
    d.addCallback(stop)

    reactor.run()


if __name__=='__main__':
    run()