User:Robert Ullmann/Rat Patrol

Definition from Wiktionary, the free dictionary
Jump to: navigation, search
Wikipedia has an article on:

Wikipedia

Contents

intro[edit]

This is a patrol program for the English Wiktionary.

It must be run from a sysop/admin account.

It requires Python + the Python Wikipedia framework. It also requires Tkinter, which usually comes with Python.

Updated September 2009, re-written version.

code[edit]

copy everything after the source tag to the end of the page, save as ratpatrol.py in the pywikipedia directory/folder

<source lang="python">

  1. !/usr/bin/python
  2. -*- coding: utf-8 -*-

""" this application patrols the en.wikt

requires some version of the "pywikipedia framework", preferably recent, but need not be updated all the time (;-), most code is within this file

requires Python 2.6

requires Tkinter (which you should have with Python)

in user-config.py, make sure one line reads:

sysopnames['wiktionary']['en'] = "(your username)"

then run! will exit more cleanly if you only use the "quit" button, rather than closing the window

the "console" (on Windoze) or the command shell you started from will display lots of log messages to tell you what it is doing

Buttons:

Edit page -- open a new tab (or window) in default browser in page edit Show diffs -- open a new tab etc showing diffs for revision Skip user -- skip edits by this user for 24 hours Whitelist -- mark all edits seen by by this user for 24 hours (new or old) Skip -- skip this edit for 72 hours (someone else ought to look at it) Mark -- mark this edit patrolled (;-)

large edits won't fit in the differences boxes, use the "show diffs" button to see more the revision list at the bottom will often tell you if a later editor fixed/reverted a bad edit, which you can then mark

"""

import wikipedia import sys import re import pickle import time import xmlreader import socket from math import floor from Tkinter import * import Queue import threading from webbrowser import open_new_tab from difflib import ndiff, unified_diff import urllib import config

def srep(s):

   return repr(u+s)[2:-1]

plock = threading.Lock()

def log(s):

   with plock: print srep(s)


  1. slightly odd string syntax in the following is so that it can be saved in wikitext

def unescape(s):

   if '&' not in s: return s
   s = s.replace("&" "lt;", "<")
   s = s.replace("&" "gt;", ">")
   s = s.replace("&" "apos;", "'")
   s = s.replace("&" "quot;", '"')
   s = s.replace("&" "#039;", "'")
   s = s.replace("&" "amp;", "&") # Must be last
   return s

site = None Quit = False

  1. ------------------------------------------------------------------------------------------------

class Task():

   def __init__(t, revid = , pid = , title = , user = , oldid = ,
                   rcid = , ts = , summary = ):
       t.revid = revid
       t.pid = pid
       t.title = title
       t.urlname = urllib.quote(title.encode(site.encoding()))
       t.user = user
       t.oldid = oldid
       t.rcid = rcid
       t.ts = ts
       t.summary = summary
       t.allrevs = None
       t.oldlines = 
       t.newlines = 
       t.revlines = 
       t.reason = 
       t.done = False
   def __cmp__(s, o):
       # inverse, decreasing [ temp back to increase ]
       if s.ts < o.ts: return -1
       if s.ts > o.ts: return 1
       return 0
  1. timeout set: a set that elements magically disappear from after a time
  2. [may need to add thread locks]

from weakref import WeakValueDictionary from heapq import heappush, heappop

class tmo(float): pass

class timeoutset():

   def __init__(s, timeout):
       s.timeout = timeout
       s.wdict = WeakValueDictionary()
       s.theap = []
   def add(s, key):
       t = tmo(time.clock())
       s.wdict[key] = t
       heappush(s.theap, t)
   def __contains__(s, key):
       while s.theap and s.theap[0] < time.clock() - s.timeout: heappop(s.theap)
       return key in s.wdict
   def __len__(s):
       return len(s.theap)
  1. all active tasks, not in skip list, key is revid

active = WeakValueDictionary()

  1. tasks skipped for a few days, values are revids

skipped = timeoutset(96 * 3600)

  1. whitelisted users

whitelist = timeoutset(24 * 3600)

  1. users to skip for a while

skipusers = timeoutset(24 * 3600)

  1. queues: tasks, ready to present, ready to mark

tasq = Queue.PriorityQueue() readyq = Queue.Queue() patrolq = Queue.Queue()

  1. prompt "queue": one task rechecked and ready to display

promptq = Queue.Queue(1)

  1. check a task to see where it should go, used at several steps

def checktask(task):

   if task.done: return None # drops task from active presumably
   if task.user in whitelist:
       task.reason = 'user ' + task.user + ' in whitelist'
       patrolq.put(task)
       return None
   if task.title in whitelistpages:
       task.reason = 'page in whitelist'
       patrolq.put(task)
       return None
   if task.user in skipusers:
       skipped.add(task.revid)
       stat("Skipped", len(skipped))
       return None
   return task # next step as normal
  1. fairly static list of white listed pages (some also annoying to load lots of revs, very big ;-)

whitelistpages = [ 'Wiktionary:Requests for verification',

                   'Wiktionary:Requests for deletion',
                   'Wiktionary:Requests for cleanup',
                   'Wiktionary:Tea room',
                   'Wiktionary:Beer parlour',
                   'Wiktionary:Grease pit',
                   'Wiktionary:Requested entries:English',
                   'Wiktionary:Requested entries:Spanish',
                   'Wiktionary:List of protologisms',
                   'Wiktionary:Translation requests',
                   'Wiktionary:Featured word candidates',
                   'Wiktionary:Feedback',
                   'Wiktionary:Information desk',
                   'Wiktionary:Sandbox',
                   'Wiktionary talk:Sandbox' ]


  1. ------------------------------------------------------------------------------------------------
  1. mwapi interface, a few mods here

from StringIO import StringIO from gzip import GzipFile

  1. first, our own read url routine, so we can accept gzip, and be much faster:

class MyURLopener(urllib.FancyURLopener):

   version="PythonWikipediaBot/1.0"
  1. since we aren't using the framework 'throttle', do something better
  2. this is a "tick-tock" timer, shared on all threads
  3. clicked down each success, up on each network failure of any type

ticktocklock = threading.Lock() ticktock = 1.0 def getticktock():

   global ticktock
   return ticktock

relagged = re.compile(r'<error.*"maxlag".* (\d+) seconds')

def readapi(site, request, sysop = True, nomaxlag = False):

   global ticktocklock, ticktock
   url = "http://" + site.hostname() + "/w/api.php?" + request
   done = False
   nap = 5
   maxl = 5
   maxlag = "&maxlag=%d" % maxl
   if nomaxlag: maxlag = 
   with ticktocklock:
       ticktock *= 0.95  # is -0.025 if 5 seconds, -1.0 at 20 seconds
       ticktock = max(ticktock, 0.1)
       ticktock = min(ticktock, 20.0)
       if ticktock >= 10.0:
           with plock: print "(mwapi readapi: tick tock is %.1f)" % ticktock
       time.sleep(ticktock)
       ticktock -= 1.0   # undo first increment in loop
   while not done:
       ticktock += 1.0   # done w/o lock, race condition is rare, not a serious problem, ignored!
       try:
           uo = MyURLopener()
           uo.addheader('Cookie', site.cookies(sysop = sysop) or )
           uo.addheader('Accept-Encoding', 'gzip')
           f = uo.open(url + maxlag)
           text = f.read()
           try:
               if 'gzip' in f.info()['Content-Encoding']:
                   text = GzipFile(fileobj=StringIO(text)).read()
           except KeyError:
               pass
           text = unicode(text, 'UTF-8' , errors = 'ignore')
           done = True
       except Exception, e:
           """ report all errors for now:
           if '10054' in repr(e) and nap < 15:
               time.sleep(nap)
               continue # quietly
           """
           with plock:
               print "(%s: exception reading API: %s)" % (threading.currentThread().name, repr(e))
           text = 
           time.sleep(nap)
           nap = min(nap + nap/2, 300)
           continue
       # following is specific to the net I am on; won't break anything for anyone else
       # so don't worry about it; but you can remove this block if desired
       if '<api' not in text and 'NdxICC' in text:
           # silently ignore bad return from Nomadix box
           time.sleep(5)
           done = False
           continue
       # use MW "server lag" feature to slow when servers are under heavy load:
       mo = relagged.search(text)
       if mo:
           replag = int(mo.group(1))
           with plock: print "(%s: server lagged %s seconds)" % \
                          (threading.currentThread().name, replag)
           # allow more lag the next time
           maxl += max(maxl/4, replag/20)
           maxlag = "&maxlag=%d" % maxl
           # make some progress even when server crocked ...
           if maxl > 600: maxlag = ""
           if maxlag and maxl > 60:
               with plock: print "(mwapi readapi: next with %s)" % maxlag
           # sleep replag if not more than 70
           time.sleep(min(replag, 70))
           done = False
           continue
   return text


  1. ------------------------------------------------------------------------------------------------
  1. recent changes, stuff into task queue

def readrc():

   # keep track of recently seen revids, they can appear to be un-patrolled in the reply
   # but we've just now done them ...
   seen = timeoutset(7200)
   # use regex. this will break sometimes if API is changed, so we will fix it (:-)
   rerev = re.compile(r'<rc [^>]*title="([^"]*)" rcid="([^"]*)" ' +
                      r'pageid="([^"]*)" revid="([^"]*)" old_revid="([^"]*)" user="([^"]*)"' +
                      r'[^>]*timestamp="([^"]*)" comment="([^"]*)"')
   restar = re.compile(r'rcstart="([^"]*)"')
   nap = 0
   rcstart =  # start at maxage
   # temp:
   # rcstart = "&rcstart=2009-09-19T00:00:00Z"
   lastr = time.time()
   while not Quit:
       nf = 0
       with plock: print "reading rc"
       rcs = readapi(site, "action=query&list=recentchanges&rcprop=title|ids|user|timestamp|comment" +
                       "&rclimit=max&rcshow=!patrolled&rcdir=newer&format=xml" + rcstart)
       for mo in rerev.finditer(rcs):
            title = unescape(mo.group(1))
            rcid = mo.group(2)
            pid = mo.group(3)
            revid = mo.group(4)
            oldid = mo.group(5)
            if oldid == "0": oldid = ""
            user = unescape(mo.group(6))
            ts = mo.group(7)
            summary = unescape(mo.group(8))
            if revid in active: continue
            if revid in skipped: continue
            if revid in seen: continue
            seen.add(revid)
            # probably a good task
            nf += 1
            task = Task(title=title, user=user, revid=revid, oldid=oldid,
                        rcid=rcid, pid=pid, ts=ts, summary=summary)
            active[revid] = task
            stat("Unpatrolled", len(active))
            task = checktask(task)
            if task: tasq.put(task)
       task = None
       # next timestamp forward (not given when we catch up to current)
       mo = restar.search(rcs)
       if mo:
           rcstart = "&rcstart=" + mo.group(1)
       else:
           # 40 minutes ago (must be >> max naptime)
           rcstart = '&rcstart=' + time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(time.time() - 2400))
       # housekeeping (things that otherwise don't get updated when timed out)
       stat("Whitelist users", len(whitelist))
       stat("Skipped users", len(skipusers))
       stat("Unpatrolled", len(active))
       # every few hours or so, start at beginning again (pick up skips, anything missed)
       if lastr < time.time() - (4 * 3600):
            rcstart = 
            lastr = time.time()
       # nap time ...
       if nf > 2:
            nap /= 2
       else:
            nap = min(nap+20, 350)
       with plock: print "rc found", nf, "next in", nap
       for i in range(nap/5):
            time.sleep(5)
            if Quit: break
   with plock: print "recent changes thread ends"

  1. ------------------------------------------------------------------------------------------------
  1. read patrol log

def readpl():

   markedothers = 0
   repat = re.compile(r'<item [^>]*title="([^"]*)"[^>]*user="([^"]*)"[^>]*>' +
                      r'\s*<patrol[^>]*prev="([^"]*)" cur="([^"]*)"')
   restar = re.compile(r'lestart="([^"]*)"')
   # start a while ago
   lestart = '&lestart=' + time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(time.time() - 2400))
   nap = 10
   while not Quit:
       nf = 0
       if len(active):
           with plock: print "reading patrol log"
           pats = readapi(site, "action=query&list=logevents&letype=patrol&lelimit=max&format=xml" +
                                "&ledir=newer" + lestart)
       else: pats =  # little point, eh? sleep some more ...
       for mo in repat.finditer(pats):
           title = unescape(mo.group(1))
           user = unescape(mo.group(2))
           prev = mo.group(3) # oldid
           cur = mo.group(4) # revid
           task = None
           if cur in active:
               try:
                   task = active[cur]
               except KeyError:
                   pass # race with GC
           if not task or task.done: continue
           with plock: print "rev %s of %s patrolled by %s" % (srep(cur), srep(title), srep(user))
           task.done = True # we have no idea where it is ... (:-)
           markedothers += 1
           stat("Marked by others", markedothers)
           nf += 1
       task = None
       # next timestamp forward (not given when we catch up to current)
       mo = restar.search(pats)
       if mo:
           lestart = "&lestart=" + mo.group(1)
       else:
           # 40 minutes ago (must be >> max naptime)
           lestart = '&lestart=' + time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(time.time() - 2400))
       if nf: nap /= 2
       else: nap = min(nap+20, 700)
       for i in range(0, nap/5):
            if Quit: break
            time.sleep(5)
   with plock: print "read patrol log thread ends"


  1. ------------------------------------------------------------------------------------------------
  1. mark edits

def patrol():

   markedbyme = 0
   whitelisted = 0
   while not Quit:
       try:
           task = patrolq.get(timeout=20)
       except Queue.Empty:
           continue
       if task.done: 
           task = None
           continue # can be marked and patrolled by someone else
       # set here, race with read log, that often picks it before we get reply!
       task.done = True
       resp = site.getUrl("/w/index.php?title=%s&action=markpatrolled&rcid=%s" % \
             (task.urlname, task.rcid), sysop = True)
       if "Marked as patrolled" not in resp:
          log("failed to patrol %s of %s" % (task.rcid, task.title))
          for line in resp.splitlines(): print srep(line)
          task = None
          time.sleep(30)
          # will presumably pick it up again eventually, if not marked by another
          # if it did in fact succeed, no matter ...
          continue
       if not task.reason:
           markedbyme += 1
           stat("Marked by me", markedbyme)
           with plock: print "patrolled %s of %s, my mark" % (srep(task.revid), srep(task.title))
       else: # presume was marked by whitelisting user or page
           whitelisted += 1
           stat("Whitelisted", whitelisted)
           with plock: 
               print "patrolled %s of %s, %s" % \
                     (srep(task.revid), srep(task.title), srep(task.reason))
       task = None
       stat("Unpatrolled", len(active))
       time.sleep(5) # no hurry
   with plock: print "patrol thread ends"


  1. ------------------------------------------------------------------------------------------------
  1. preload, read from task q, write to ready q

def preload():

   rever = re.compile(r'<rev revid="([^"]*)"[^>]*user="([^"]*)"' +
                      r'[^>]*timestamp="([^"]*)"\s*(comment="[^"]*"|)[^>]*>(.*?)</rev>', re.S)


   # cache of previously read revisions, kept as long as some other task has them
   # key is title, value is other task (can't just be unicode string, must be object)
   revcache = WeakValueDictionary()
   while not Quit:
       try:
           task = tasq.get(timeout=20)
       except Queue.Empty:
           continue
       task = checktask(task)
       if not task: continue
       log('preload for ' + task.title)
       # see if we have revs already
       revs = None
       if task.title in revcache:
           try:
               ot = revcache[task.title]
               revs = ot.allrevs # from other task
               ot = None
           except KeyError: pass
       if not revs:
           with plock: print "reading 20 revs for", srep(task.title)
           revs = readapi(site,
                "action=query&prop=revisions|info&rvprop=timestamp|user|comment|content|ids&format=xml"
                "&titles=" + task.urlname + "&rvlimit=20")
       # now we want to see if we have enough; maybe not, and maybe stale
       if 'revid="' + task.revid + '"' not in revs: revs = 
       if task.oldid and 'revid="' + task.oldid + '"' not in revs: revs = 
       # if not, do it again at 200
       if not revs:
           with plock: print "reading 200 revs for", srep(task.title)
           revs = readapi(site,
                "action=query&prop=revisions|info&rvprop=timestamp|user|comment|content|ids&format=xml"
                "&titles=" + task.urlname + "&rvlimit=200")
       # check again!
       if 'revid="' + task.revid + '"' not in revs: revs = 
       if task.oldid and 'revid="' + task.oldid + '"' not in revs: revs = 
       if not revs:
            # can happen on page deletes, moves, stuff
            with plock: "can't find needed old revs for %s, skipping task!" % srep(task.title)
            skipped.add(task.revid)
            stat("Skipped", len(skipped))
            task = None
            stat("Unpatrolled", len(active))
            continue
       task.allrevs = revs
       revcache[task.title] = task
       # now available for other tasks on same title
       # now find the revs we want, and make a list ...
       oldrevtext = 
       newrevtext = 
       replinelist = [ ]
       for mo in rever.finditer(revs):
            revid = mo.group(1)
            if revid == task.revid: mark = '*'
            else: mark = ' '
            user = unescape(mo.group(2))
            ts = mo.group(3)
            ts = ts.replace('T', ' ')
            ts = ts.replace('Z', ' ')
            comment = unescape(mo.group(4)[9:-1])
            text = unescape(mo.group(5))
            if len(replinelist) < 10:
                replinelist.append("%s %s %s: (%s)" % (ts, mark, user, comment))
                # with plock: print "debug match rev", srep(ts), srep(user), srep(comment)
            elif revid == task.revid:
                # add to list with ellipsis (;-)
                replinelist = replinelist[:-2]
                replinelist.append("       [ ... ]")
                replinelist.append("%s %s %s: (%s)" % (ts, mark, user, comment))
            if revid == task.revid: newrevtext = text
            if revid == task.oldid: oldrevtext = text
       # should have always been found?
       if not newrevtext:
           with plock: print "what? can't match new revtext in revs?"
           continue # discard task?
       task.revlines = u'\n'.join(replinelist)
       # differences
       """ old
       for delta in ndiff(oldrevtext.splitlines(), newrevtext.splitlines()):
           delta = unescape(delta)
           if delta.startswith('- '):
               task.oldlines += delta[2:] + '\n'
           elif delta.startswith('+ '):
               task.newlines += delta[2:] + '\n'
           # ignore '  ' and '? ' lines, might do something with context later?
       """
       for delta in unified_diff(oldrevtext.splitlines(), newrevtext.splitlines(), n=1):
           delta = unescape(delta)
           if delta.startswith('--'): pass
           elif delta.startswith('++'): pass
           elif delta.startswith('-'):
               task.oldlines += delta[1:] + '\n'
           elif delta.startswith('+'):
               task.newlines += delta[1:] + '\n'
           elif delta.startswith(' '):
               task.oldlines += delta[1:] + '\n'
               task.newlines += delta[1:] + '\n'
           # ignore other lines
       readyq.put(task)
       task = None
       nap = readyq.qsize() / 20
       for i in range(nap/5):
           time.sleep(5)
           if Quit: break
   with plock: print "preload thread ends"
  1. ------------------------------------------------------------------------------------------------
  1. recheck task, just before presenting to user
  2. reverted edits don't show as patrolled in the log
  3. and page may be (often is) deleted
  4. so we look for one row in the RC table to tell us if the edit is still there
  1. has to run as a thread or it hangs the UI
  2. takes a task off of the readyq and stuffs it in the prompt "queue" (placeholder for one entry)
  1. note we can't select on rcid (which would be cool, and obvious), so we look for everything
  2. with the exact timestamp

reclines = re.compile(r'<rc[^>]*rcid="(\d*)"[^>]*>')

def recheck():

 while not Quit:
   # get a new task:
   try:
       task = readyq.get_nowait()
   except Queue.Empty:
       time.sleep(5)
       continue
   task = checktask(task)
   if not task or task.done:
       task = None
       continue
   rcs = readapi(site, "action=query&list=recentchanges&rcprop=ids|patrolled" +
                 "&rclimit=20&format=xml&rcstart=%s&rcend=%s" % (task.ts, task.ts), nomaxlag = True)
   # print srep(rcs)
   for mo in reclines.finditer(rcs):
       if mo.group(1) != task.rcid: continue
       if "patrolled" in mo.group(0):
           with plock: print "rev %s of %s patrolled by someone" % (srep(task.revid), srep(task.title))
           task = None
       else:
           promptq.put(task)
           task = None
           while promptq.qsize() > 0: time.sleep(1) # wait for it to be eaten
       break
   # not found?
   if task:
       with plock: print "rev %s of %s apparently deleted" % (srep(task.revid), srep(task.title))
       task = None
   continue
 with plock: print "recheck thread ends"
  1. ------------------------------------------------------------------------------------------------
  1. now the tkinter stuff:
  2. could do a fancy class with attributes and methods, but instead keep it simple, just share some stuff

root = None status = None statboxes = { }

tkmessages = Queue.Queue()

def stat(n, v): tkmessages.put( (n, v) )

  1. messages to update stats from other threads

def tkmess():

   while tkmessages.qsize():
       try:
           lab, val = tkmessages.get()
           statboxes[lab].config(text=val)
           statboxes[lab].update_idletasks()
       except Queue.Empty:
           pass
   root.after(200, tkmess)
  1. oldest (?) unpatrolled page data
  1. shared things, just easier this way, all belong to tk run thread

oldboxes = { } oldedit = None # current task being presented oldlines = None newlines = None revlines = None showdiffb = None oldlineslabel = None newlineslabel = None

def get_next_oldpage():

   global oldboxes, oldedit
   global showdiffb, oldlineslabel, newlineslabel
   oldedit = None
   oldboxes['Title'].config(text=)
   oldboxes['User'].config(text=)
   oldboxes['Summary'].config(text=)
   oldlines.config(text=)
   newlines.config(text=)
   revlines.config(text=)
   try:
       oldedit = promptq.get_nowait()
   except Queue.Empty:
       root.after(5000, get_next_oldpage)
       return
   oldedit = checktask(oldedit)
   if not oldedit:
       # recall immediately:
       root.after(20, get_next_oldpage)
       return
       
   if oldedit.oldid:
       oldlineslabel.config(text='Old lines')
       newlineslabel.config(text='New lines')
       showdiffb.config(text='Show diffs')
   else:
       oldlineslabel.config(text=)
       newlineslabel.config(text='Page text')
       showdiffb.config(text='Show page')
   oldboxes['Title'].config(text=oldedit.title)
   oldboxes['User'].config(text=oldedit.user)
   oldboxes['Summary'].config(text=oldedit.summary)
   oldlines.config(text=oldedit.oldlines)
   newlines.config(text=oldedit.newlines)
   revlines.config(text=oldedit.revlines)
   newlines.update_idletasks()
   return

def mark_edit():

   if not oldedit: return
   patrolq.put(oldedit)
   get_next_oldpage()
   return

def show_diff():

   if not oldedit: return
   if oldedit.oldid:
       open_new_tab("http://en.wiktionary.org/w/index.php?title=%s&diff=next&oldid=%s" % 
              (oldedit.urlname, oldedit.oldid))
   else:
       open_new_tab("http://en.wiktionary.org/w/index.php?title=%s" % oldedit.urlname)


   return

def edit_page():

   if not oldedit: return
   open_new_tab("http://en.wiktionary.org/w/index.php?title=%s&action=edit" % oldedit.urlname)
   return

def skip_edit():

   global oldedit
   if not oldedit: return
   skipped.add(oldedit.revid)
   stat("Skipped", len(skipped))
   oldedit = None
   stat("Unpatrolled", len(active))
   get_next_oldpage()
   return

def skip_user():

   global oldedit
   if not oldedit: return
   skipusers.add(oldedit.user)
   stat("Skipped users", len(skipusers))
   # continue to skip this edit
   skip_edit()
   return

def whitelist_user():

   global oldedit
   if not oldedit: return
   whitelist.add(oldedit.user)
   stat("Whitelist users", len(whitelist))
   # continue to mark this edit
   mark_edit()
   return

def rats_quit():

   global Quit
   Quit = True
   log("Quitting ...")
   time.sleep(20)
   root.quit()
  1. main program runs tkinter loop

def main():

   global site
   socket.setdefaulttimeout(140)
   site = wikipedia.getSite("en", "wiktionary")
   site.forceLogin(sysop = True)
   # force this off, sysop is never bot (framework bug)
   config.notify_unflagged_bot = False
   # things shared with subs
   global root, oldlines, newlines, revlines
   global showdiffb, oldlineslabel, newlineslabel
   rct = threading.Thread(target=readrc)
   rct.daemon = True
   rct.name = 'read recent changes'
   plt = threading.Thread(target=readpl)
   plt.daemon = True
   plt.name = 'read patrol log'
   prt = threading.Thread(target=preload)
   prt.daemon = True
   prt.name = 'preload'
   pat = threading.Thread(target=patrol)
   pat.daemon = True
   pat.name = 'mark patrol'
   rkt = threading.Thread(target=recheck)
   rkt.daemon = True
   rkt.name = 'recheck revision'
   root = Tk()
   root.title('Rat Patrol')
   font = ('Arial', 10)
   fontb = ('Arial', 10, 'bold')
   # pack from bottom, then left to right at top:
   revlines = Label(root, width=97, height=10, justify=LEFT, anchor=W, font=font, bg='#fff', relief=RIDGE)
   revlines.pack(side=BOTTOM, padx=5, pady=5)
   # button bar
   bbox = Frame(root)
   bbox.pack(side=BOTTOM, fill=X, padx=10, pady=5)
   editpageb = Button(bbox, text="Edit page", width=11, font=font, command=edit_page)
   editpageb.pack(side=LEFT)
   showdiffb = Button(bbox, text="Show diffs", width=11, font=font, command=show_diff)
   showdiffb.pack(side=LEFT)
   skipuserb = Button(bbox, text="Skip user", width=11, font=font, command=skip_user)
   skipuserb.pack(side=LEFT)
   wluserb = Button(bbox, text="Whitelist", width=11, font=font, command=whitelist_user)
   wluserb.pack(side=LEFT)
   skipeditb = Button(bbox, text="Skip", width=8, font=font, command=skip_edit)
   skipeditb.pack(side=LEFT)
   markeditb = Button(bbox, text="Mark", width=8, font=font, command=mark_edit)
   markeditb.pack(side=LEFT)
   quitb = Button(bbox, text='Quit', width=10, font=font, command=rats_quit)
   quitb.pack(side=RIGHT)
   # differences
   dbox = Frame(root)
   dbox.pack(side=BOTTOM, padx=10, pady=5)
   obox = Frame(dbox)
   obox.pack(side=LEFT)
   oldlineslabel = Label(obox, text="Old lines", width=24, font=fontb, justify=LEFT, anchor=W)
   oldlineslabel.pack(side=TOP, fill=X)
   oldlines = Label(obox, width=48, height=8, justify=LEFT, anchor=W, font=font, bg='#fff', relief=RIDGE)
   oldlines.config(wraplength = oldlines.winfo_reqwidth() - 8)
   oldlines.pack(side=TOP)
   nbox = Frame(dbox)
   nbox.pack(side=LEFT)
   newlineslabel = Label(nbox, text="New lines", width=24, font=fontb, justify=LEFT, anchor=W)
   newlineslabel.pack(side=TOP, fill=X)
   newlines = Label(nbox, width=48, height=8, justify=LEFT, anchor=W, font=font, bg='#fff', relief=RIDGE)
   newlines.config(wraplength = newlines.winfo_reqwidth() - 8)
   newlines.pack(side=TOP)
   # statistics frame, boxes
   stats = Frame(root)
   stats.pack(side=LEFT, padx=10, pady=5)
   statframes = { }
   statlabels = { }
   for lab in [ 'Unpatrolled', 'Marked by me', 'Marked by others', 'Whitelisted', \
                'Whitelist users', 'Skipped users', 'Skipped' ]:
       statframes[lab] = Frame(stats)
       statframes[lab].pack(side=TOP)
       statlabels[lab] = Label(statframes[lab], text=lab+':', width=15, font=font, justify=LEFT, anchor=W)
       statlabels[lab].pack(side=LEFT)
       statboxes[lab] = Label(statframes[lab], text='0', width=10, font=font, justify=RIGHT, anchor=E, bg='#fff', relief=RIDGE)
       statboxes[lab].pack(side=RIGHT)
   ebox = Frame(root)
   ebox.pack(side=LEFT, padx=10, pady=5, fill=X)
   oldframes = { }
   oldlabels = { }
   oldtoplabel = Label(ebox, text="Oldest unpatrolled edit", width=24, font=fontb, justify=LEFT, anchor=W)
   oldtoplabel.pack(side=TOP, fill=X)
   for lab in [ 'Title', 'User', 'Summary' ]:
       oldframes[lab] = Frame(ebox)
       oldframes[lab].pack(side=TOP)
       oldlabels[lab] = Label(oldframes[lab], text=lab+':', width=15, font=font, justify=LEFT, anchor=NW)
       oldlabels[lab].pack(side=LEFT)
       oldboxes[lab] = Label(oldframes[lab], text=, width=48, font=font, justify=LEFT, anchor=W, bg='#fff', relief=RIDGE)
       oldboxes[lab].pack(side=RIGHT)
   oldlabels['Summary'].config(text='Summary:\n\n\n')  # (hack ;-)
   oldboxes['Summary'].config(height = 4)
   oldboxes['Summary'].config(wraplength = oldboxes['Summary'].winfo_reqwidth() - 8)
   root.after(200, tkmess)
   root.after(200, get_next_oldpage)
   rct.start()
   plt.start()
   prt.start()
   pat.start()
   rkt.start()
   root.mainloop()

if __name__ == "__main__":

   try:
       main()
   finally:
       wikipedia.stopme()