User:Robert Ullmann/Rat Patrol

intro
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
copy everything after the source tag to the end of the page, save as ratpatrol.py in the pywikipedia directory/folder


 * 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



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)

active = WeakValueDictionary
 * 1) all active tasks, not in skip list, key is revid

skipped = timeoutset(96 * 3600)
 * 1) tasks skipped for a few days, values are revids

whitelist = timeoutset(24 * 3600)
 * 1) whitelisted users

skipusers = timeoutset(24 * 3600)
 * 1) users to skip for a while


 * 1) queues: tasks, ready to present, ready to mark

tasq = Queue.PriorityQueue readyq = Queue.Queue patrolq = Queue.Queue

promptq = Queue.Queue(1)
 * 1) prompt "queue": one task rechecked and ready to display


 * 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) 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'= 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 ' 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) 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']*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) read patrol log

def readpl:

markedothers = 0

repat = re.compile(r']*title="([^"]*)"[^>]*user="([^"]*)"[^>]*>' +                      r'\s*]*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) 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) preload, read from task q, write to ready q

def preload:

rever = re.compile(r']*user="([^"]*)"' +                      r'[^>]*timestamp="([^"]*)"\s*(comment="[^"]*"|)[^>]*>(.*?) ', 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) 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']*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) 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

oldboxes = { } oldedit = None # current task being presented oldlines = None newlines = None revlines = None showdiffb = None oldlineslabel = None newlineslabel = None
 * 1) shared things, just easier this way, all belong to tk run thread

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