[py-svn] r10293 - in py/branch/py-collect: bin test/tkinter test/tkinter/icons

jan at codespeak.net jan at codespeak.net
Mon Apr 4 17:32:41 CEST 2005


Author: jan
Date: Mon Apr  4 17:32:41 2005
New Revision: 10293

Added:
   py/branch/py-collect/bin/py.test.tkinter   (contents, props changed)
   py/branch/py-collect/test/tkinter/
   py/branch/py-collect/test/tkinter/__init__.py
   py/branch/py-collect/test/tkinter/gui.py
   py/branch/py-collect/test/tkinter/guidriver.py
   py/branch/py-collect/test/tkinter/icons/
   py/branch/py-collect/test/tkinter/icons/gray.gif   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/green.gif   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/green.png   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/green_aqua.gif   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/green_aqua.png   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/green_s.gif   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/green_s.png   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/green_s_aqua.gif   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/green_s_aqua.png   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/red.gif   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/red.png   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/red_aqua.gif   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/red_aqua.png   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/red_c.gif   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/red_c.png   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/red_c_aqua.gif   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/red_c_aqua.png   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/yellow.gif   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/yellow.png   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/yellow_aqua.gif   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/yellow_aqua.png   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/yellow_s.gif   (contents, props changed)
   py/branch/py-collect/test/tkinter/icons/yellow_s.png   (contents, props changed)
   py/branch/py-collect/test/tkinter/repository.py
   py/branch/py-collect/test/tkinter/test_exceptionfailure.py
   py/branch/py-collect/test/tkinter/test_guidriver.py
   py/branch/py-collect/test/tkinter/test_repository.py
   py/branch/py-collect/test/tkinter/test_utils.py
   py/branch/py-collect/test/tkinter/tkgui.py
   py/branch/py-collect/test/tkinter/tktree.py
   py/branch/py-collect/test/tkinter/utils.py
Log:
First checkin of a Tkinter frontend for py.test.
Start with py.test.tkinter.



Added: py/branch/py-collect/bin/py.test.tkinter
==============================================================================
--- (empty file)
+++ py/branch/py-collect/bin/py.test.tkinter	Mon Apr  4 17:32:41 2005
@@ -0,0 +1,5 @@
+#!/usr/bin/env python 
+
+from _findpy import py 
+from py.__impl__.test.tkinter import tkgui
+tkgui.main() 

Added: py/branch/py-collect/test/tkinter/__init__.py
==============================================================================

Added: py/branch/py-collect/test/tkinter/gui.py
==============================================================================
--- (empty file)
+++ py/branch/py-collect/test/tkinter/gui.py	Mon Apr  4 17:32:41 2005
@@ -0,0 +1,180 @@
+from tktree import Tree, Node
+from utils import TestReport, Status, Null
+from Tkinter import PhotoImage
+import py
+Item = py.test.Item
+Collector = py.test.Collector
+
+
+class ResultTree(Tree):
+
+    def __init__(self, *args, **kwargs):
+        kwdict = {'dist_x': 19, 'dist_y': 19,
+                  'selection_callback': Null(),
+                  'double_click_callback': Null()}
+        kwdict.update(kwargs)
+        self.set_selection_callback(kwdict['selection_callback'])
+        self.set_double_click_callback(kwdict['double_click_callback'])
+        del kwdict['selection_callback']
+        del kwdict['double_click_callback']
+        self.setup_icons()
+        
+        Tree.__init__(self, *args, **kwdict)
+        self.save_view()
+        self.save_selection()
+        self.bind('<4>', self.prev)
+        self.bind('<5>', self.next)
+
+    def setup_icons(self):
+        iconpath = py.magic.autopath().dirpath('icons')
+        self.icon_green = PhotoImage(file = str(iconpath.join('green.gif')))
+        self.icon_red = PhotoImage(file =  str(iconpath.join('red.gif')))
+        self.icon_yellow = PhotoImage(file =  str(iconpath.join('yellow.gif')))
+        self.icon_gray = PhotoImage(file =  str(iconpath.join('gray.gif')))
+
+        self.icon_map = {str(Status.NotExecuted()): self.icon_gray,
+                         str(Status.Passed()): self.icon_green,
+                         str(Status.Failed()): self.icon_red,
+                         str(Status.Skipped()): self.icon_yellow,
+                         str(Status.ExceptionFailure()): self.icon_red}
+
+    def get_icon(self, status):
+        status = str(status)
+        return self.icon_map[status]
+
+    def add_node(self, *args, **kwargs):
+        report = kwargs.setdefault('report', TestReport())
+        icon = self.get_icon(report.status)
+        kwdict = {'collapsed_icon': icon, 'expanded_icon': icon}
+        del kwargs['report']
+        kwdict.update(kwargs)
+        Tree.add_node(self, *args, **kwdict)
+
+    def set_selection_callback(self, callback):
+        self._selection_callback = callback
+
+    def set_double_click_callback(self, callback):
+        self._double_click_callback = callback
+
+    def double_click_callback(self, node = None):
+        self._double_click_callback(self.pos)
+
+    def save_view(self):
+        self.saved_view = self.get_expanded_state()
+
+    def save_selection(self):
+        self.saved_selection_id =  self.cursor_node(None).full_id()
+
+    def restore_view(self):
+        self.apply_expanded_state(self.saved_view)
+
+    def restore_selection(self):
+        if self.find_full_id(self.saved_selection_id):
+                self.move_cursor(self.find_full_id(self.saved_selection_id))
+
+    def next(self, event = None):
+        Tree.next(self, event)
+        self.save_selection()
+        
+    def prev(self, event = None):
+        Tree.prev(self, event)
+        self.save_selection()
+
+
+    def move_cursor(self, node):
+        Tree.move_cursor(self, node)
+        self._selection_callback(node)
+
+    def listnodes(self, startnode = None):
+        if startnode is None:
+            startnode = self.root
+        ret = [startnode]
+        for child in startnode.children():
+            ret.append(child)
+            ret.extend(self.listnodes(child))
+        return ret
+        
+
+    def get_expanded_state(self, key = None):
+        "returns [node.full_id() for all_nodes if  node.expanded() == True"
+        if key is None:
+            key = self.root.full_id()
+        if not self.find_full_id(key) or not self.find_full_id(key).expanded():
+            return []
+        startnode = self.find_full_id(key)
+        return [node.full_id() for node in self.listnodes(startnode) if node.expanded()]
+
+    def apply_expanded_state(self, id_list):
+        ids = [item for item in id_list]
+        ids.sort()
+        for id in ids:
+            node = self.find_full_id(id)
+            if node is None or not node.expandable():
+                continue
+            node.expand()
+            pass
+
+    def get_parent(self, node_id):
+        node = self.find_full_id(node_id)
+        if node:
+            return node.parent()
+        return self.find_full_id(node_id[:-1])
+
+    def update_root(self, report):
+        self.root.set_collapsed_icon(self.get_icon(report.status))
+        self.root.set_expanded_icon(self.get_icon(report.status))
+        self.root.set_label(report.label)
+               
+    def update_node(self, node_id):
+        if node_id == self.root.full_id() or self.get_parent(node_id) is None:
+            parent = self.root
+        else:
+            parent = self.get_parent(node_id)
+
+        if not parent.expandable():
+            self.update_node(parent.full_id())
+            return
+        
+        if parent.expanded():
+            parent.toggle_state()
+            parent.toggle_state()
+            self.restore_view()
+            self.restore_selection()
+        
+             
+        
+class ResultNode(Node):
+
+    def __init__(self, *args, **kwargs):
+        self.args = args
+        self.kwargs = kwargs
+        Node.__init__(self, *args, **kwargs)
+        self.widget.tag_bind(self.symbol, '<Button-1>',
+                             self.PVT_click_select, add='+')
+        self.widget.tag_bind(self.label, '<Button-1>',
+                             self.PVT_click_select, add='+')
+        self.widget.tag_bind(self.symbol, '<Double-Button-1>',
+                             self.PVT_double_click, add='+')
+        self.widget.tag_bind(self.label, '<Double-Button-1>',
+                             self.PVT_double_click, add='+')
+        
+        
+        
+    def get_node_args(self):
+        return self.args, self.kwargs
+
+    def PVT_click(self, event):
+        Node.PVT_click(self, event)
+        self.widget.save_selection()
+        self.widget.save_view()
+        
+    def PVT_click_select(self, event):
+        # not interested in dnd, so just do a select
+        self.widget.move_cursor(self)
+        self.widget.save_selection()
+
+    def PVT_double_click(self, event):
+        self.widget.double_click_callback(self)
+        
+
+

Added: py/branch/py-collect/test/tkinter/guidriver.py
==============================================================================
--- (empty file)
+++ py/branch/py-collect/test/tkinter/guidriver.py	Mon Apr  4 17:32:41 2005
@@ -0,0 +1,100 @@
+
+import py
+from utils import Status, TestReport, Null
+from py.__impl__.test.drive import Exit, exit, SimpleOutErrCapture
+
+import pprint
+
+class GuiDriver(py.test.Driver):
+
+    def __init__(self, option = None, channel = None):
+        super(GuiDriver, self).__init__(option)
+        self.channel = channel
+        self.reportlist = []
+
+    def info(self, **kwargs):
+        return
+        print '-' * 60
+        #print 'reportlist:', self.reportlist
+        for key, value in kwargs.items():
+            print key,':', pprint.pformat(value)
+        if isinstance(kwargs.get('res'), (py.test.Item.Passed,
+                                          py.test.Item.Skipped,
+                                          py.test.Item.Failed)):
+            print '!' *80
+            print 'py.test.Collector.Outcome found'
+            print '!' *80
+        print '-' * 60
+        
+    def header(self, colitems):
+        super(GuiDriver, self).header(colitems)
+        report = TestReport()
+        report.settime()
+        self.reportlist = [report]
+        self.sendreport(report)
+        
+    def footer(self, colitems):
+        super(GuiDriver, self).footer(colitems)
+        report = self.reportlist.pop()
+        report.settime()
+        self.sendreport(report)
+        self.channel.send(None)
+
+    def start(self, colitem):
+        super(GuiDriver, self).start(colitem)
+        report = TestReport()
+        report.start(colitem)
+        self.reportlist.append(report)
+        self.sendreport(report)
+        
+
+    def finish(self, colitem, res):
+        super(GuiDriver, self).finish(colitem, res)
+        report = self.reportlist.pop()
+        report.finish(colitem, res, self.option)
+        self.reportlist[-1].status.update(report.status)
+        self.sendreport(report)
+        #py.std.time.sleep(0.5)
+        
+    def sendreport(self, report):
+        self.channel.send(report.toChannel())
+
+    def warning(self, msg):
+        pass
+
+    # hack! Driver.runone should be patched
+    def runone(self, colitem):
+        if self.shouldclose(): 
+            raise SystemExit, "received external close signal" 
+
+        capture = None
+        if not self.option.nocapture and isinstance(colitem, py.test.Item):
+            capture = SimpleOutErrCapture() 
+        res = None
+        try: 
+            self.start(colitem)
+            try: 
+                try:
+                    res = self.runinner(colitem) 
+                except (KeyboardInterrupt, Exit): 
+                    res = None
+                    raise 
+                except colitem.Outcome, res: 
+                    res.excinfo = py.code.ExceptionInfo() 
+                except: 
+                    excinfo = py.code.ExceptionInfo() 
+                    res = colitem.Failed(excinfo=excinfo) 
+                else: 
+                    assert (res is None or 
+                            isinstance(res, (list, colitem.Outcome)))
+            finally:
+                if capture is not None: 
+                    out, err = capture.reset() 
+                    try: 
+                        res.out, res.err = out, err 
+                    except AttributeError: 
+                        pass
+                self.finish(colitem, res) 
+        finally: 
+            pass
+

Added: py/branch/py-collect/test/tkinter/icons/gray.gif
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/green.gif
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/green.png
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/green_aqua.gif
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/green_aqua.png
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/green_s.gif
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/green_s.png
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/green_s_aqua.gif
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/green_s_aqua.png
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/red.gif
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/red.png
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/red_aqua.gif
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/red_aqua.png
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/red_c.gif
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/red_c.png
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/red_c_aqua.gif
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/red_c_aqua.png
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/yellow.gif
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/yellow.png
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/yellow_aqua.gif
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/yellow_aqua.png
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/yellow_s.gif
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/icons/yellow_s.png
==============================================================================
Binary file. No diff available.

Added: py/branch/py-collect/test/tkinter/repository.py
==============================================================================
--- (empty file)
+++ py/branch/py-collect/test/tkinter/repository.py	Mon Apr  4 17:32:41 2005
@@ -0,0 +1,170 @@
+import py
+Item = py.test.Item
+Collector = py.test.Collector
+
+
+
+import copy
+import time
+
+from utils import Null
+from itertools import izip, count
+
+import UserDict
+
+## import logging
+## log = logging
+## def initlog():
+##     global log
+##     remote = logging.FileHandler('./remote.log')
+##     remote.setLevel(logging.DEBUG)
+##     formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
+##     remote.setFormatter(formatter)
+##     log = logging.getLogger('remote')
+##     log.addHandler(remote)
+
+
+
+class Repository(object):
+    nothing = object()
+
+    def __init__(self):   
+        self.root = self.newnode()
+
+    def newnode(self):
+        return [self.nothing, OrderedDictMemo()]
+
+    def copy(self):
+        newrepos = Repository()
+        newrepos.root = copy.deepcopy(self.root)
+        return newrepos
+
+    def add(self, key, value):
+        node = self.root
+        for k in key:
+            node = node[1].setdefault(k, self.newnode())
+        node[0] = value
+        
+    def find_tuple(self, key=[]):
+        node = self.root
+        for k in key:
+            node = node[1][k]
+        return node
+
+    def find(self, key=[]):
+        return self.find_tuple(key)[0]
+
+    def haskey(self, key):
+        try:
+            value = self.find(key)
+        except KeyError:
+            return False
+        return True
+
+    def haskeyandvalue(self, key):
+        if self.haskey(key):
+            value = self.find(key)
+            return value is not self.nothing
+        return False
+                    
+    def find_children(self, key=[]):
+        if self.haskey(key):
+            node = self.find_tuple(key)
+            return [list(key) + [childname] for childname in node[1].keys()]
+        return []
+
+    def keys(self, startkey=[]):
+        ret = []
+        for key in self.find_children(startkey):
+            ret.append(key)
+            ret.extend(self.keys(key))
+        return ret   
+
+    def removestalekeys(self, key):
+        if self.find_children(key) == [] and not self.haskeyandvalue(key):
+            if len(key) > 0:
+                parent = self.find_tuple(key[:-1])
+                del parent[1][key[-1]]
+                self.removestalekeys(key[:-1])
+
+
+    def delete(self, key):
+        if self.haskeyandvalue(key):
+            node = self.find_tuple(key)
+            node[0] = self.newnode()[0]
+            self.removestalekeys(key)
+
+    def delete_all(self, key):
+        if self.haskeyandvalue(key):
+            node = self.find_tuple(key)
+            node[0], node[1] = self.newnode()[0], self.newnode()[1]
+            self.removestalekeys(key)
+        
+    def values(self, startkey=[]):
+        return [self.find(key) for key in self.keys(startkey)]
+
+    def items(self, startkey=[]):
+        return [(key, self.find(key)) for key in self.keys(startkey)]
+
+
+class OrderedDict(UserDict.DictMixin):
+
+    def __init__(self, *args, **kwargs):
+        self._dict = dict(*args, **kwargs)
+        self._keys = self._dict.keys()
+
+    def __getitem__(self, key):
+        return self._dict.__getitem__(key)
+
+    def __setitem__(self, key, value):
+        self._dict.__setitem__(key, value)
+        try:
+            self._keys.remove(key)
+        except ValueError:
+            pass
+        self._keys.append(key)
+        
+    def __delitem__(self, key):
+        self._dict.__delitem__(key)
+        self._keys.remove(key)
+
+    def keys(self):
+        return self._keys[:]
+
+    def copy(self):
+        new = OrderedDict()
+        for key, value in self.iteritems():
+            new[key] = value
+        return new
+
+class OrderedDictMemo(UserDict.DictMixin):
+
+    def __init__(self, *args, **kwargs):
+        self._dict = dict(*args, **kwargs)
+        self._keys = self._dict.keys()
+
+    def __getitem__(self, key):
+        return self._dict.__getitem__(key)
+
+    def __setitem__(self, key, value):
+        self._dict.__setitem__(key, value)
+        if key not in self._keys:
+            self._keys.append(key)
+        
+    def __delitem__(self, key):
+        self._dict.__delitem__(key)
+        
+    def keys(self):
+        return [key for key in self._keys if key in self._dict]
+
+    def copy(self):
+        new = OrderedDict()
+        for key, value in self.iteritems():
+            new[key] = value
+        return new
+
+
+
+
+
+

Added: py/branch/py-collect/test/tkinter/test_exceptionfailure.py
==============================================================================
--- (empty file)
+++ py/branch/py-collect/test/tkinter/test_exceptionfailure.py	Mon Apr  4 17:32:41 2005
@@ -0,0 +1,7 @@
+
+import time
+
+import py
+
+def test_exceptionfailure():
+    py.test.raises(TypeError, 'time.time()')

Added: py/branch/py-collect/test/tkinter/test_guidriver.py
==============================================================================
--- (empty file)
+++ py/branch/py-collect/test/tkinter/test_guidriver.py	Mon Apr  4 17:32:41 2005
@@ -0,0 +1,63 @@
+
+import py
+import guidriver
+GuiDriver = guidriver.GuiDriver
+
+from utils import Status, TestReport, Null
+
+class TestGuiDriver:
+
+    class ChannelMock:
+
+        def __init__(self, receinvelist = []):
+            self.reset(receinvelist)
+
+        def reset(self, receivelist = []):
+            self.receivelist = receivelist
+            self.sendlist = []
+
+        def send(self, obj):
+            self.sendlist.append(obj)
+
+        def receive(self):
+            return self.receivelist.pop(0)
+        
+    def setup_method(self, method):
+        self.channel = self.ChannelMock()
+        self.driver = GuiDriver(Null(), self.channel)
+        self.collitems = [Null(), Null()]
+    
+    def test_header_sends_report_with_id_root(self):
+        self.driver.header(self.collitems)
+        
+        assert self.channel.sendlist != []
+        report = TestReport.fromChannel(self.channel.sendlist[0])
+        assert report.status == Status.NotExecuted()
+        assert report.id == 'Root'
+        assert report.label == 'Root'
+
+    def test_footer_sends_report_and_None(self):
+        self.driver.header(self.collitems)
+        self.driver.footer(self.collitems)
+
+        assert self.channel.sendlist != []
+        assert self.channel.sendlist[-1] is None
+        report = TestReport.fromChannel(self.channel.sendlist[-2])
+        assert report.status == Status.NotExecuted()
+        assert report.id == 'Root'
+
+##     def test_status_is_passed_to_root(self):
+##         self.driver.header(self.collitems)
+##         self.driver.start(self.collitems[0])
+##         self.driver.finish(self.collitems[0], py.test.Collector.Failed())
+##         self.driver.footer(self.collitems)
+
+##         assert self.channel.sendlist[-1] is None
+##         assert self.channel.sendlist.pop() is None
+
+##         report = TestReport.fromChannel(self.channel.sendlist[-1])
+##         assert report.name == 'Root'
+##         assert report.status == Status.Failed()
+
+ 
+

Added: py/branch/py-collect/test/tkinter/test_repository.py
==============================================================================
--- (empty file)
+++ py/branch/py-collect/test/tkinter/test_repository.py	Mon Apr  4 17:32:41 2005
@@ -0,0 +1,170 @@
+
+from repository import Repository, OrderedDict, OrderedDictMemo
+import py
+Item = py.test.Item
+
+
+import itertools       
+
+
+class TestRepository:
+
+    def setup_method(self, method):
+        self.rep = Repository()
+
+    def test_add_find_single_value(self):
+        key = ['key']
+        value = 'value'
+        self.rep.add(key, value)
+        assert self.rep.find(key) == value
+
+    def test_add_works_like_update(self):
+        key = 'k e y'.split()
+        value = 'value'
+        value2 = 'value2'
+        self.rep.add(key, value)
+        self.rep.add(key, value2)
+        assert self.rep.find(key) == value2
+
+    def test_haskeyandvalue(self):
+        key = 'first_middle_last'
+        value = 'value'
+        self.rep.add(key, value)
+        assert self.rep.haskeyandvalue(key)
+        assert not self.rep.haskeyandvalue('first')
+        for index in range(1, len(key[0])):
+            assert not self.rep.haskeyandvalue(key[0:index])
+        
+    def test_add_find_subkey(self):
+        key = ('key', 'subkey')
+        value = 'subvalue'
+        self.rep.add(key, value)
+        self.rep.add((key[0],), 'value')
+        assert self.rep.find(key) == value
+
+    def test_find_raises_KeyError(self):
+        py.test.raises(KeyError, self.rep.find, 'nothing')
+
+    def test_haskey(self):
+        self.rep.add('key', 'value')
+        assert self.rep.haskey('key') ==  True
+        assert self.rep.haskey('katja') == False
+        assert self.rep.haskey('ke') == True
+
+    def test_find_children_empyt_repository(self):
+        assert self.rep.find_children() == []
+
+    def test_find_children(self):
+        self.rep.add(['c'], 'childvalue')
+        self.rep.add('c a'.split(), 'a')
+        self.rep.add('c b'.split(), 'b')
+        assert self.rep.find_children(['c']) == [ ['c','a'], ['c','b']]
+        assert self.rep.find_children() == [['c']]
+
+    def test_find_children_with_tuple_key(self):
+        key = tuple('k e y'.split())
+        value = 'value'
+        self.rep.add(key, value)
+        assert self.rep.find_children([]) == [['k']]
+        assert self.rep.find_children(('k', 'e')) == [['k', 'e', 'y']]
+
+    def test_keys(self):
+        keys = [ 'a b c'.split(), 'a b'.split(), ['a']]
+        for key in keys:
+            self.rep.add(key, 'value')
+        assert len(keys) == len(self.rep.keys())
+        for key in self.rep.keys():
+            assert key in keys
+        for key in keys:
+            assert key in self.rep.keys()
+
+    def test_delete_simple(self):
+        key = 'k'
+        value = 'value'
+        self.rep.add(key, value)
+        self.rep.delete(key)
+        assert self.rep.haskeyandvalue(key) == False
+
+
+    def test_removestallkeys_remove_all(self):
+        key = 'k e y'.split()
+        value = 'value'
+        self.rep.add(key, value)
+        node = self.rep.find_tuple(key)
+        node[0] = self.rep.newnode()[0]
+        self.rep.removestalekeys(key)
+        assert self.rep.keys() == []
+        
+    def test_removestallkeys_dont_remove_parent(self):
+        key = 'k e y'.split()
+        key2 = 'k e y 2'.split()
+        value = 'value'
+        self.rep.add(key, value)
+        self.rep.add(key2, self.rep.newnode()[0])
+        self.rep.removestalekeys(key2)
+        assert self.rep.haskey(key2) == False
+        assert self.rep.haskeyandvalue(key)
+
+    def test_removestallkeys_works_with_parameter_root(self):
+        self.rep.removestalekeys([])
+        
+    def test_copy(self):
+        key = 'k e y'.split()
+        key2 = 'k e y 2'.split()
+        value = 'value'
+        self.rep.add(key, value)
+        self.rep.add(key2, value)
+        newrep = self.rep.copy()
+        assert newrep.root is not self.rep.root
+        assert newrep.find(key) == self.rep.find(key)
+        
+
+
+class TestOrderedDict:
+
+    def setup_method(self, method):
+        self.dict = OrderedDict()
+
+    def test_add(self):
+        self.dict['key'] = 'value'
+        assert 'key' in self.dict
+
+    def test_order(self):
+        keys = range(3)
+        for k in keys:
+            self.dict[k] = str(k)
+        assert keys == self.dict.keys()
+
+class TestOrderedDictMemo(TestOrderedDict):
+
+    def setup_method(self, method):
+        self.dict = OrderedDictMemo()
+
+    def test_insert(self):
+        self.dict['key1'] = 1
+        self.dict['key2'] = 2
+        del self.dict['key1']
+        self.dict['key1'] = 1
+        assert self.dict.keys() == ['key1', 'key2']
+        
+        
+class Test_Failing:
+    """ will fail!!"""
+    
+    def test_fail(self):
+        print 'recorded stdout'
+        m = dict(((1,2),(3,4)))
+        l = (1,m)
+        assert False == l
+
+    def test_calling_fail(self):
+        py.test.fail('Try to fail')
+
+    def test_skip(self):
+        print 'recorded stdout'
+        py.test.skip('Try to skip this test')
+
+    def test_skip_on_error(self):
+        py.test.skip_on_error(''.index, 'hello')
+
+    

Added: py/branch/py-collect/test/tkinter/test_utils.py
==============================================================================
--- (empty file)
+++ py/branch/py-collect/test/tkinter/test_utils.py	Mon Apr  4 17:32:41 2005
@@ -0,0 +1,118 @@
+
+import utils
+from utils import Status, TestReport, OutBuffer
+import py
+Item = py.test.Item
+
+class TestStatus:
+
+    def test_init_with_None(self):
+        status = Status(None)
+        assert status == status.NotExecuted()
+
+    def test_str(self):
+        status = Status(Item.Passed())
+        assert status == status.Passed()
+
+        status = Status(Item.Failed())
+        assert status == status.Failed()
+
+        status = Status(Item.Skipped())
+        assert status == status.Skipped()
+
+        status = Status(Item.ExceptionFailure())
+        assert status == status.ExceptionFailure()
+
+
+    def test_init_with_bad_name(self):
+        status = Status('nothing')
+        assert status == Status.NotExecuted()
+
+    def test_init_with_good_name(self):
+        def check_str(obj, expected):
+            assert str(obj) == expected
+            
+        for name in Status.ordered_list:
+            yield check_str, Status(name), name
+
+    def test_update(self):
+        failed = Status.Failed()
+        passed = Status.Passed()
+        failed.update(passed)
+        assert failed == Status.Failed()
+
+        passed.update(failed)
+        assert passed == Status.Failed()
+        assert passed == failed
+
+    def test_eq_(self):
+        passed = Status.Passed()
+        assert passed == passed
+        assert passed == Status.Passed()
+
+        failed = Status.Failed()
+        assert failed != passed
+
+
+class TestTestReport:
+
+    def setup_method(self, method):
+        self.path = py.path.local()
+        self.collector = py.test.Directory(self.path)
+        self.testresult = TestReport()
+        
+    def test_start(self):
+        self.testresult.start(self.collector)
+
+        assert self.testresult.full_id == self.collector.listnames()
+        assert self.testresult.time != 0
+        assert self.testresult.status == Status.NotExecuted()
+
+    def test_finish(self):
+        self.testresult.start(self.collector)
+
+        py.std.time.sleep(1.1)
+
+        self.testresult.finish(self.collector, None)
+        assert self.testresult.time > 1
+        assert self.testresult.status == Status.NotExecuted()
+
+##     def test_finish_failed(self):
+##         self.testresult.start(self.collector)
+
+##         self.testresult.finish(self.collector, py.test.Collector.Failed())
+##         assert self.testresult.status == Status.Failed()
+
+        
+        
+    def test_toChannel_fromChannel(self):
+        assert isinstance(self.testresult.toChannel()['status'], str)
+        result = TestReport.fromChannel(self.testresult.toChannel())
+        assert isinstance(result.status, Status)
+
+    def test_copy(self):
+        result2 = self.testresult.copy()
+        assert self.testresult.status == Status.NotExecuted()
+        for key in TestReport.template.keys():
+            assert getattr(result2, key) == getattr(self.testresult, key)
+
+        self.testresult.status = Status.Failed()
+        assert result2.status != self.testresult.status
+        
+            
+class Test_OutBuffer:
+
+    def setup_method(self, method):
+        self.out = OutBuffer()
+
+    def test_line(self):
+        oneline = 'oneline'
+        self.out.line(oneline)
+        assert self.out.getoutput() == oneline + '\n'
+
+    def test_write(self):
+        item = 'item'
+        self.out.write(item)
+        assert self.out.getoutput() == item
+
+    

Added: py/branch/py-collect/test/tkinter/tkgui.py
==============================================================================
--- (empty file)
+++ py/branch/py-collect/test/tkinter/tkgui.py	Mon Apr  4 17:32:41 2005
@@ -0,0 +1,366 @@
+import Tkinter
+tk = Tkinter
+import ScrolledText
+import time
+import threading
+import Queue
+import sys
+import os
+import re
+import sha
+
+import py
+from py.test import Driver
+Item = py.test.Item
+Collector = py.test.Collector
+
+import repository
+import utils
+from utils import TestReport, Status, Null
+import gui
+
+
+class TkMain:
+    def __init__(self, parent, queue, endcommand, startcommand, repositorycommand, manager = Null()):
+        self.parent = parent
+        self.queue = queue
+        self.endcommand = endcommand
+        self.startcommand = startcommand
+        self.repositorycommand = repositorycommand
+        self.manager = manager
+        # GUI
+        self.text = Null()
+        self.createwidgets()
+        start = Tkinter.Button(parent, text='Start', command=startcommand)
+        start.pack(side = tk.TOP)
+        done = Tkinter.Button(parent, text='Done', command=endcommand)
+        done.pack(side = tk.TOP)
+        
+
+    def createwidgets(self):
+        self.mainframe = tk.Frame(self.parent)
+        self.treeframe = tk.Frame(self.mainframe)
+        self.tree = gui.ResultTree(master=self.treeframe, root_id='Root',
+                                   get_contents_callback=self.create_new_nodes,
+                                   width=400, node_class = gui.ResultNode,
+                                   selection_callback=self.selection_callback,
+                                   double_click_callback = self.double_click_callback)
+        sb=tk.Scrollbar(self.treeframe)
+        sb.pack(side=tk.RIGHT, fill = tk.Y)
+        self.tree.configure(yscrollcommand=sb.set)
+        sb.configure(command=self.tree.yview)
+        
+        sb=tk.Scrollbar(self.treeframe, orient=tk.HORIZONTAL)
+        sb.pack(side=tk.BOTTOM, fill = tk.X)
+        self.tree.configure(xscrollcommand=sb.set)
+        sb.configure(command=self.tree.xview)
+        
+        self.tree.pack(side = tk.LEFT, expand=tk.YES, fill = tk.BOTH)
+        self.treeframe.pack(side = tk.TOP, expand = tk.YES, fill = tk.BOTH)
+
+        
+        self.text = ScrolledText.ScrolledText(self.mainframe)
+        self.text.pack(side = tk.TOP, expand=tk.YES, fill = tk.BOTH)
+        self.mainframe.pack(side = tk.LEFT, expand = tk.YES, fill = tk.BOTH)
+        
+    def double_click_callback(self, node):
+        self.startcommand(rerun_ids=[node.full_id()])
+
+    def selection_callback(self, node):
+        # check if node exists
+        if not  self.repositorycommand().haskey(node.full_id()):
+            return
+        report = self.repositorycommand().find(node.full_id())
+        self.text['state'] = tk.NORMAL
+        self.text.delete(1.0, tk.END)
+        self.text.tag_config('sel', relief=tk.FLAT)
+        self.text.insert(tk.END, report.error_report)
+        #if len(report.error_report.splitlines()) < 20:
+        #    self.text.config(height=len(report.error_report.splitlines()) + 5)
+        #self.text.yview_pickplace(tk.END)
+        self.attacheditorhotspots(self.text)
+        self.text['state'] = tk.DISABLED
+        pass
+
+    def attacheditorhotspots(self, text):
+        # Attach clickable regions to a Text widget.
+        filelink = re.compile(r"""\[(?:testcode\s*:)?\s*(.+):(\d+)\]""")
+        skippedlink = re.compile(r"""in\s+(/.*):(\d+)\s+""")
+        lines = text.get('1.0', tk.END).splitlines(1)
+        if not lines:
+            return
+        tagname = ''
+        start, end = 0,0
+        for index, line in enumerate(lines):
+            match = filelink.search(line)
+            if match is None:
+                match = skippedlink.search(line)
+            if match is None:
+                continue
+            file, line = match.group(1, 2)
+            start, end = match.span()
+            tagname = "ref%d" % index
+            text.tag_add(tagname,
+                         "%d.%d" % (index + 1, start),
+                         "%d.%d" % (index + 1, end))
+            text.tag_bind(tagname, "<Enter>",
+                          lambda e, n=tagname:
+                          e.widget.tag_config(n, underline=1))
+            text.tag_bind(tagname, "<Leave>",
+                          lambda e, n=tagname:
+                          e.widget.tag_config(n, underline=0))
+            text.tag_bind(tagname, "<Button-1>",
+                          lambda e, self=self, f=file, l=line:
+                          self.launch_editor(f, l))
+                    
+    def launch_editor(self, file, line):
+        editor = (py.std.os.environ.get('PYUNIT_EDITOR', None) or
+                  py.std.os.environ.get('EDITOR_REMOTE', None) or
+                  os.environ.get('EDITOR', None) or "emacsclient --no-wait ")
+                  #"emacsclient --no-wait ")
+        if editor:
+            print "%s +%s %s" % (editor, line, file)
+            py.process.cmdexec('%s +%s %s' % (editor, line, file))
+            
+
+    def create_new_nodes(self, node):
+        repos = self.repositorycommand()
+        id = node.full_id()
+        for child in repos.find_children(id):
+            if not repos.haskeyandvalue(child):
+                continue
+            report = repos.find(child)
+            name =  report.label    
+            folder = 0
+            if repos.find_children(child):
+                folder = 1
+            self.tree.add_node(name=name, id=report.id, flag=folder, report = report)
+
+    def display_action(self, action):
+        self.parent.title(str(action))
+                
+    def process_messages(self, manager):
+        """
+        Handle all the messages currently in the queue (if any).
+        """
+        while self.queue.qsize():
+            try:
+                full_id = self.queue.get(0)
+                if full_id is None:
+                    root_status = self.repositorycommand().find(self.tree.root.full_id()).status
+                    self.display_action(root_status)
+                    if root_status != Status.Passed():
+                        self.parent.bell()
+                        py.std.time.sleep(0.2)
+                        self.parent.bell()
+                else:
+                    self.tree.update_node(full_id)
+                    self.display_action('running: ' + str(full_id[-1]) )
+            except Queue.Empty:
+                pass
+        self.tree.update_root(self.repositorycommand().find(self.tree.root.full_id()))
+
+    
+class Manager:
+    """
+    Launch the main part of the GUI and the worker thread. periodicCall and
+    endApplication could reside in the GUI part, but putting them here
+    means that you have all the thread controls in a single place.
+    """
+    def __init__(self, parent):
+        """
+        Start the GUI and the asynchronous threads. We are in the main
+        (original) thread of the application, which will later be used by
+        the GUI.
+        """
+        self.parent = parent
+
+        self.reset_repository()
+        self.args = py.std.sys.argv[1:]
+        self.paths = py.test.config.init(self.args)[1]
+        self.testfilewatcher = utils.TestFileWatcher(*self.paths)
+        # Create the queue
+        self.guiqueue = Queue.Queue()
+        self.reporterqueue = Queue.Queue()
+        
+        self.should_stop = False
+        
+        # Set up the GUI part
+        self.gui = TkMain(parent, self.guiqueue, self.endApplication, self.start_tests, self.getrepository, manager = self)
+
+        # Set up the thread to do asynchronous I/O
+        self.thread = Null()
+        # Start the periodic call in the GUI to check if the queue contains
+        # anything
+        self.check_messages()
+
+    def reset_repository(self, ids = []):
+        if not ids:
+            self.repository = self.get_new_repository()
+            return
+        for id in ids:
+            self.repository.delete_all(id)
+        for key, value in self.get_new_repository().items():
+            if not self.repository.haskey(key):
+                self.repository.add(key, value)
+         
+
+    def get_new_repository(self):
+        new_repository = repository.Repository()
+        template = TestReport()
+        new_repository.add(['Root'], template.copy())
+        for name in [str(x) for x in (Status.Failed(),
+                                      Status.Skipped(),
+                                      Status.ExceptionFailure())]:
+            new_repository.add(['Root', name], template.copy(id = name, label= name))
+        return new_repository
+    
+
+    def thread_is_alive(self):
+        return self.thread.isAlive() == True
+    running = property(thread_is_alive)
+
+
+    def getrepository(self):
+        return self.repository
+
+
+    def check_messages(self):
+        """
+        Check every 200 ms if there is something new in the queue.
+        """
+        while self.reporterqueue.qsize():
+            try:
+                report_fromChannel = self.reporterqueue.get(0)
+                if report_fromChannel is None: 
+                    self.guiqueue.put(None)
+                else:
+                    report = TestReport.fromChannel(report_fromChannel)
+                    root_id = TestReport().full_id
+                    # keep root up-to-date
+                    self.repository.find(root_id).status.update(report.status)
+                    if report.full_id == root_id:
+                        # don't overwrite overall status on partial run
+                        report.status = Status(self.repository.find(root_id).status)                    
+                    else:
+                        # hack: prefix Root for all Tests, except Root
+                        report.full_id = ['Root'] + report.full_id
+                    id = report.full_id
+                    self.repository.add(id, report)
+                    added_name = self.add_status(report)
+                    if added_name is not None:
+                        self.guiqueue.put(added_name)
+                    self.guiqueue.put(id)
+            except Queue.Empty:
+                pass
+            
+        self.gui.process_messages(self)
+        if self.should_stop:
+            # This is the brutal stop of the system. You may want to do
+            # some cleanup before actually shutting it down.
+            import sys
+            sys.exit(1)
+        if not self.running and self.testfilewatcher.changed():
+            self.start_tests()
+        self.parent.after(200, self.check_messages)
+
+    def add_status(self, newreport):
+        report = newreport.copy()
+        status = report.status
+        if report.error_report:
+            folder = ('Root', str(report.status))
+            # unique id, for failing tests with the same name
+            report.id = report.id + str(report.full_id)
+            id = folder + (report.id,)
+            self.repository.find(folder).status.update(report.status)
+            self.repository.add(id, report)
+            return id
+
+    def endApplication(self):
+        self.should_stop = True
+        #print 'System is shutting down now!'
+        #print 'join thread %s' % self.thread
+        self.thread.join()
+        #print 'Thread is down'
+
+
+    def get_restart_params_list(self, id):
+        repos = self.getrepository()
+        if repos.find(id).restart_params is not None:
+            return [repos.find(id).restart_params]
+        params = []
+        for child_id in repos.find_children(id):
+            params.extend(self.get_restart_params_list(child_id))
+        return params
+        
+    def start_tests(self, filenames = [], rerun_ids =  []):
+        if self.running:
+            return      
+        rerun_cols = []
+        for id in rerun_ids:
+            rerun_cols.extend(self.get_restart_params_list(id))
+        
+        self.reset_repository(rerun_ids)
+        if filenames == []:
+            filenames = [str(p) for p in self.paths]
+        kwargs = {'rerun_cols': rerun_cols}
+        if rerun_cols == []:
+            kwargs['rerun_cols'] = [(filename,[]) for filename in filenames]
+            
+        self.thread = threading.Thread(target=self.run_tests_threaded,
+                                       kwargs = kwargs)
+        self.thread.start()
+       
+
+    def run_tests_threaded(self, rerun_cols = None):
+        gw = py.execnet.PopenGateway(sys.executable)
+        try:
+            channel = gw.remote_exec("""
+            import py
+            from py.__impl__.test.tkinter.guidriver import GuiDriver
+            from py.__impl__.test.tkinter import repository
+            import os
+            import time
+            from py.__impl__.test.run import FailureCollector
+            
+            rerun_cols, args = channel.receive()
+            col = FailureCollector(rerun_cols)
+            # hack!!
+            frontenddriver, paths = py.test.config.init(args)
+            driver = GuiDriver(option = frontenddriver.option, channel = channel)
+            driver.run(col)
+            """)
+            channel.send((rerun_cols, self.args))
+            self.receive_all_data(channel)
+            self.channel_waitclose(channel)
+        finally:
+            channel.close()
+            channel.gateway.exit()
+
+    def receive_all_data(self, channel):
+        while True:
+            data = channel.receive()
+            self.reporterqueue.put(data)
+            if data is None:
+                break
+            
+
+    def channel_waitclose(self, channel):
+        while 1:
+            try:
+                channel.waitclose(0.1)
+            except (IOError, py.error.Error):
+                continue
+            break
+
+def main():
+    root = Tkinter.Tk()
+
+    client = Manager(root)
+    root.protocol('WM_DELETE_WINDOW', client.endApplication)
+    root.mainloop()
+
+
+if __name__ == '__main__':
+    main()
+

Added: py/branch/py-collect/test/tkinter/tktree.py
==============================================================================
--- (empty file)
+++ py/branch/py-collect/test/tkinter/tktree.py	Mon Apr  4 17:32:41 2005
@@ -0,0 +1,911 @@
+# Highly optimized Tkinter tree control
+# by Charles E. "Gene" Cash <email gcash at cfl.rr.com>
+#
+# This is documented more fully on my homepage at
+# http://home.cfl.rr.com/genecash/ and if it's not there, look in the Vaults
+# of Parnassus at http://www.vex.net/parnassus/ which I promise to keep
+# updated.
+#
+# Thanks to Laurent Claustre <claustre at esrf.fr> for sending lots of helpful
+# bug reports.
+#
+# This copyright license is intended to be similar to the FreeBSD license. 
+#
+# Copyright 1998 Gene Cash All rights reserved. 
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    1. Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#    2. Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the
+#       distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GENE CASH ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# This means you may do anything you want with this code, except claim you
+# wrote it. Also, if it breaks you get to keep both pieces.
+#
+# 02-DEC-98 Started writing code.
+# 22-NOV-99 Changed garbage collection to a better algorithm.
+# 28-AUG-01 Added logic to deal with exceptions in user callbacks.
+# 02-SEP-01 Fixed hang when closing last node.
+# 07-SEP-01 Added binding tracking so nodes got garbage-collected.
+#           Also fixed subclass call to initialize Canvas to properly deal
+#           with variable arguments and keyword arguments.
+# 11-SEP-01 Bugfix for unbinding code.
+# 13-OCT-01 Added delete & insert methods for nodes (by email request).
+#           LOTS of code cleanup.
+#           Changed leading double underscores to PVT nomenclature.
+#           Added ability to pass Node subclass to Tree constructor.
+#           Removed after_callback since subclassing Node is better idea.
+# 15-OCT-01 Finally added drag'n'drop support.  It consisted of a simple
+#           change to the Node PVT_click method, and addition of logic like
+#           the example in Tkdnd.py.  It took 3 days to grok the Tkdnd
+#           example and 2 hours to make the code changes.  Plus another 1/2
+#           day to get a working where() function.
+# 16-OCT-01 Incorporated fixes to delete() and dnd_commit() bugs by
+#           Laurent Claustre <claustre at esrf.fr>.
+# 17-OCT-01 Added find_full_id() and cursor_node() methods.
+# 18-OCT-01 Fixes to delete() on root during collapse and with
+#           drag-in-progress flag by Laurent Claustre <claustre at esrf.fr>.
+# 10-FEB-02 Fix to prev_visible() by Nicolas Pascal <pascal at esrf.fr>.
+#           Fixes which made insert_before()/insert_after() actually work.
+#           Also added expand/collapse indicators like Internet Explorer
+#           as requested by Nicolas.
+# 11-FEB-02 Another fix to prev_visible().  It works this time.  Honest.
+# 31-MAY-02 Added documentation strings so the new PYthon 2.2 help function
+#           is a little more useful.
+# 19-AUG-02 Minor fix to eliminate crash in "treedemo-icons.py" caused by
+#           referencing expand/collapse indicators when lines are turned off.
+# 15-OCT-02 Used new idiom for calling Canvas superclass.
+# 18-NOV-02 Fixed bug discovered by Amanjit Gill <amanjit.gill at gmx.de>, where
+#           I didn't pass "master" properly to the Canvas superclass. Sigh.
+#           One step forward, one step back.
+
+import Tkdnd
+from Tkinter import *
+
+#------------------------------------------------------------------------------
+def report_callback_exception():
+    """report exception on sys.stderr."""
+    import traceback
+    import sys
+    
+    sys.stderr.write("Exception in Tree control callback\n")
+    traceback.print_exc()
+    
+#------------------------------------------------------------------------------
+class Struct:
+    """Helper object for add_node() method"""
+    def __init__(self):
+        pass
+
+#------------------------------------------------------------------------------
+class Node:
+    """Tree helper class that's instantiated for each element in the tree.  It
+    has several useful attributes:
+    parent_node     - immediate parent node
+    id              - id assigned at creation
+    expanded_icon   - image displayed when folder is expanded to display
+                      children
+    collapsed_icon  - image displayed when node is not a folder or folder is
+                      collapsed.
+    parent_widget   - reference to tree widget that contains node.
+    expandable_flag - is true when node is a folder that may be expanded or
+                      collapsed.
+    expanded_flag   - true to indicate node is currently expanded.
+    h_line          - canvas line to left of node image.
+    v_line          - canvas line below node image that connects children.
+    indic           - expand/collapse canvas image.
+    label           - canvas text label
+    symbol          - current canvas image
+
+    Please note that methods prefixed PVT_* are not meant to be used by
+    client programs."""
+    
+    def __init__(self, parent_node, id, collapsed_icon, x, y,
+                 parent_widget=None, expanded_icon=None, label=None,
+                 expandable_flag=0):
+        """Create node and initialize it.  This also displays the node at the
+        given position on the canvas, and binds mouseclicks."""
+        # immediate parent node
+        self.parent_node=parent_node
+        # internal name used to manipulate things
+        self.id=id
+        # bitmaps to be displayed
+        self.expanded_icon=expanded_icon
+        self.collapsed_icon=collapsed_icon
+        # tree widget we belong to
+        if parent_widget:
+            self.widget=parent_widget
+        else:
+            self.widget=parent_node.widget
+        # for speed
+        sw=self.widget
+        # our list of child nodes
+        self.child_nodes=[]
+        # flag that node can be expanded
+        self.expandable_flag=expandable_flag
+        self.expanded_flag=0
+        # add line
+        if parent_node and sw.line_flag:
+            self.h_line=sw.create_line(x, y, x-sw.dist_x, y)
+        else:
+            self.h_line=None
+        self.v_line=None
+        # draw approprate image
+        self.symbol=sw.create_image(x, y, image=self.collapsed_icon)
+        # add expand/collapse indicator
+        self.indic=None
+        if expandable_flag and sw.line_flag and sw.plus_icon and sw.minus_icon:
+            self.indic=sw.create_image(x-sw.dist_x, y, image=sw.plus_icon)
+        # add label
+        self.label=sw.create_text(x+sw.text_offset, y, text=label, anchor='w')
+        # single-click to expand/collapse
+        if self.indic:
+            sw.tag_bind(self.indic, '<1>', self.PVT_click)
+        else:
+            sw.tag_bind(self.symbol, '<1>', self.PVT_click)
+        ## Modification: symbol always clickable
+        #sw.tag_bind(self.symbol, '<1>', self.PVT_click)
+
+        # for drag'n'drop target detection
+        sw.tag_bind(self.symbol, '<Any-Enter>', self.PVT_enter)
+        sw.tag_bind(self.label, '<Any-Enter>', self.PVT_enter)
+
+    # for testing (gotta make sure nodes get properly GC'ed)
+    #def __del__(self):
+    #    print self.full_id(), 'deleted'
+
+    # ----- PUBLIC METHODS -----
+    def set_collapsed_icon(self, icon):
+        """Set node's collapsed image"""
+        self.collapsed_icon=icon
+        if not self.expanded_flag:
+            self.widget.itemconfig(self.symbol, image=icon)
+
+    def set_expanded_icon(self, icon):
+        """Set node's expanded image"""
+        self.expanded_icon=icon
+        if self.expanded_flag:
+            self.widget.itemconfig(self.symbol, image=icon)
+
+    def parent(self):
+        """Return node's parent node"""
+        return self.parent_node
+
+    def prev_sib(self):
+        """Return node's previous sibling (the child immediately above it)"""
+        i=self.parent_node.child_nodes.index(self)-1
+        if i >= 0:
+            return self.parent_node.child_nodes[i]
+        else:
+            return None
+
+    def next_sib(self):
+        """Return node's next sibling (the child immediately below it)"""
+        i=self.parent_node.child_nodes.index(self)+1
+        if i < len(self.parent_node.child_nodes):
+            return self.parent_node.child_nodes[i]
+        else:
+            return None
+        
+    def next_visible(self):
+        """Return next lower visible node"""
+        n=self
+        if n.child_nodes:
+            # if you can go right, do so
+            return n.child_nodes[0]
+        while n.parent_node:
+            # move to next sibling
+            i=n.parent_node.child_nodes.index(n)+1
+            if i < len(n.parent_node.child_nodes):
+                return n.parent_node.child_nodes[i]
+            # if no siblings, move to parent's sibling
+            n=n.parent_node
+        # we're at bottom
+        return self
+    
+    def prev_visible(self):
+        """Return next higher visible node"""
+        n=self
+        if n.parent_node:
+            i=n.parent_node.child_nodes.index(n)-1
+            if i < 0:
+                return n.parent_node
+            else:
+                j=n.parent_node.child_nodes[i]
+                return j.PVT_last()
+        else:
+            return n
+                
+    def children(self):
+        """Return list of node's children"""
+        return self.child_nodes[:]
+
+    def get_label(self):
+        """Return string containing text of current label"""
+        return self.widget.itemcget(self.label, 'text')
+
+    def set_label(self, label):
+        """Set current text label"""
+        self.widget.itemconfig(self.label, text=label)
+
+    def expanded(self):
+        """Returns true if node is currently expanded, false otherwise"""
+        return self.expanded_flag
+
+    def expandable(self):
+        """Returns true if node can be expanded (i.e. if it's a folder)"""
+        return self.expandable_flag
+    
+    def full_id(self):
+        """Return list of IDs of all parents and node ID"""
+        if self.parent_node:
+            return self.parent_node.full_id()+(self.id,)
+        else:
+            return (self.id,)
+
+    def expand(self):
+        """Expand node if possible"""
+        if not self.expanded_flag:
+            self.PVT_set_state(1)
+        
+    def collapse(self):
+        """Collapse node if possible"""
+        if self.expanded_flag:
+            self.PVT_set_state(0)
+
+    def delete(self, me_too=1):
+        """Delete node from tree. ("me_too" is a hack not to be used by
+        external code, please!)"""
+        sw=self.widget
+        if not self.parent_node and me_too:
+            # can't delete the root node
+            raise ValueError, "can't delete root node"
+        self.PVT_delete_subtree()
+        # move everything up so that distance to next subnode is correct
+        n=self.next_visible()
+        x1, y1=sw.coords(self.symbol)
+        x2, y2=sw.coords(n.symbol)
+        if me_too:
+            dist=y2-y1
+        else:
+            dist=y2-y1-sw.dist_y
+        self.PVT_tag_move(-dist)
+        n=self
+        if me_too:
+            if sw.pos == self:
+                # move cursor if it points to current node
+                sw.move_cursor(self.parent_node)
+            self.PVT_unbind_all()
+            sw.delete(self.symbol)
+            sw.delete(self.label)
+            sw.delete(self.h_line)
+            sw.delete(self.v_line)
+            sw.delete(self.indic)
+            self.parent_node.child_nodes.remove(self)
+            # break circular ref now, so parent may be GC'ed later
+            n=self.parent_node
+            self.parent_node=None
+        n.PVT_cleanup_lines()
+        n.PVT_update_scrollregion()
+
+    def insert_before(self, nodes):
+        """Insert list of nodes as siblings before this node.  Call parent
+        node's add_node() function to generate the list of nodes."""
+        i=self.parent_node.child_nodes.index(self)
+        self.parent_node.PVT_insert(nodes, i, self.prev_visible())
+    
+    def insert_after(self, nodes):
+        """Insert list of nodes as siblings after this node.  Call parent
+        node's add_node() function to generate the list of nodes."""
+        i=self.parent_node.child_nodes.index(self)+1
+        self.parent_node.PVT_insert(nodes, i, self.PVT_last())
+        
+    def insert_children(self, nodes):
+        """Insert list of nodes as children of this node.  Call node's
+        add_node() function to generate the list of nodes."""
+        self.PVT_insert(nodes, 0, self)
+        
+    def toggle_state(self):
+        """Toggle node's state between expanded and collapsed, if possible"""
+        if self.expandable_flag:
+            if self.expanded_flag:
+                self.PVT_set_state(0)
+            else:
+                self.PVT_set_state(1)
+                
+    # ----- functions for drag'n'drop support -----
+    def PVT_enter(self, event):
+        """detect mouse hover for drag'n'drop"""
+        self.widget.target=self
+        
+    def dnd_end(self, target, event):
+        """Notification that dnd processing has been ended. It DOES NOT imply
+        that we've been dropped somewhere useful, we could have just been
+        dropped into deep space and nothing happened to any data structures,
+        or it could have been just a plain mouse-click w/o any dragging."""
+        if not self.widget.drag:
+            # if there's been no dragging, it was just a mouse click
+            self.widget.move_cursor(self)
+            self.toggle_state()
+        self.widget.drag=0
+
+    # ----- PRIVATE METHODS (prefixed with "PVT_") -----
+    # these methods are subject to change, so please try not to use them
+    def PVT_last(self):
+        """Return bottom-most node in subtree"""
+        n=self
+        while n.child_nodes:
+            n=n.child_nodes[-1]
+        return n
+    
+    def PVT_find(self, search):
+        """Used by searching functions"""
+        if self.id != search[0]:
+            # this actually only goes tilt if root doesn't match
+            return None
+        if len(search) == 1:
+            return self
+        # get list of children IDs
+        i=map(lambda x: x.id, self.child_nodes)
+        # if there is a child that matches, search it
+        try:
+            return self.child_nodes[i.index(search[1])].PVT_find(search[1:])
+        except:
+            return None
+
+    def PVT_insert(self, nodes, pos, below):
+        """Create and insert new children. "nodes" is list previously created
+        via calls to add_list(). "pos" is index in the list of children where
+        the new nodes are inserted. "below" is node which new children should
+        appear immediately below."""
+        if not self.expandable_flag:
+            raise TypeError, 'not an expandable node'
+        # for speed
+        sw=self.widget
+        # expand and insert children
+        children=[]
+        self.expanded_flag=1
+        sw.itemconfig(self.symbol, image=self.expanded_icon)
+        if sw.minus_icon and sw.line_flag:
+            sw.itemconfig(self.indic, image=sw.minus_icon)
+        if len(nodes):
+            # move stuff to make room
+            below.PVT_tag_move(sw.dist_y*len(nodes))
+            # get position of first new child
+            xp, dummy=sw.coords(self.symbol)
+            dummy, yp=sw.coords(below.symbol)
+            xp=xp+sw.dist_x
+            yp=yp+sw.dist_y
+            # create vertical line
+            if sw.line_flag and not self.v_line:
+                self.v_line=sw.create_line(
+                    xp, yp,
+                    xp, yp+sw.dist_y*len(nodes))
+                sw.tag_lower(self.v_line, self.symbol)
+            n=sw.node_class
+            for i in nodes:
+                # add new subnodes, they'll draw themselves
+                # this is a very expensive call
+                children.append(
+                    n(parent_node=self, expandable_flag=i.flag, label=i.name,
+                      id=i.id, collapsed_icon=i.collapsed_icon,
+                      expanded_icon=i.expanded_icon, x=xp, y=yp))
+                yp=yp+sw.dist_y
+            self.child_nodes[pos:pos]=children
+            self.PVT_cleanup_lines()
+            self.PVT_update_scrollregion()
+            sw.move_cursor(sw.pos)
+        
+    def PVT_set_state(self, state):
+        """Common code forexpanding/collapsing folders. It's not re-entrant,
+        and there are certain cases in which we can be called again before
+        we're done, so we use a mutex."""
+        while self.widget.spinlock:
+            pass
+        self.widget.spinlock=1
+        # expand & draw our subtrees
+        if state:
+            self.child_nodes=[]
+            self.widget.new_nodes=[]
+            if self.widget.get_contents_callback:
+                # this callback needs to make multiple calls to add_node()
+                try:
+                    self.widget.get_contents_callback(self)
+                except:
+                    report_callback_exception()
+            self.PVT_insert(self.widget.new_nodes, 0, self)
+        # collapse and delete subtrees
+        else:
+            self.expanded_flag=0
+            self.widget.itemconfig(self.symbol, image=self.collapsed_icon)
+            if self.indic:
+                self.widget.itemconfig(self.indic, image=self.widget.plus_icon)
+            self.delete(0)
+        # release mutex
+        self.widget.spinlock=0
+
+    def PVT_cleanup_lines(self):
+        """Resize connecting lines"""
+        if self.widget.line_flag:
+            n=self
+            while n:
+                if n.child_nodes:
+                    x1, y1=self.widget.coords(n.symbol)
+                    x2, y2=self.widget.coords(n.child_nodes[-1].symbol)
+                    self.widget.coords(n.v_line, x1, y1, x1, y2)
+                n=n.parent_node
+
+    def PVT_update_scrollregion(self):
+        """Update scroll region for new size"""
+        x1, y1, x2, y2=self.widget.bbox('all')
+        self.widget.configure(scrollregion=(x1, y1, x2+5, y2+5))
+
+    def PVT_delete_subtree(self):
+        """Recursively delete subtree & clean up cyclic references to make
+        garbage collection happy"""
+        sw=self.widget
+        sw.delete(self.v_line)
+        self.v_line=None
+        for i in self.child_nodes:
+            # delete node's subtree, if any
+            i.PVT_delete_subtree()
+            i.PVT_unbind_all()
+            # delete widgets from canvas
+            sw.delete(i.symbol)
+            sw.delete(i.label)
+            sw.delete(i.h_line)
+            sw.delete(i.v_line)
+            sw.delete(i.indic)
+            # break circular reference
+            i.parent_node=None
+        # move cursor if it's in deleted subtree
+        if sw.pos in self.child_nodes:
+            sw.move_cursor(self)
+        # now subnodes will be properly garbage collected
+        self.child_nodes=[]
+        
+    def PVT_unbind_all(self):
+        """Unbind callbacks so node gets garbage-collected. This wasn't easy
+        to figure out the proper way to do this.  See also tag_bind() for the
+        Tree widget itself."""
+        for j in (self.symbol, self.label, self.indic, self.h_line,
+                  self.v_line):
+            for k in self.widget.bindings.get(j, ()):
+                self.widget.tag_unbind(j, k[0], k[1])
+
+    def PVT_tag_move(self, dist):
+        """Move everything below current icon, to make room for subtree using
+        the Disney magic of item tags.  This is the secret of making
+        everything as fast as it is."""
+        # mark everything below current node as movable
+        bbox1=self.widget.bbox(self.widget.root.symbol, self.label)
+        bbox2=self.widget.bbox('all')
+        self.widget.dtag('move')
+        self.widget.addtag('move', 'overlapping', 
+                           bbox2[0], bbox1[3], bbox2[2], bbox2[3])
+        # untag cursor & node so they don't get moved too
+        self.widget.dtag(self.widget.cursor_box, 'move')
+        self.widget.dtag(self.symbol, 'move')
+        self.widget.dtag(self.label, 'move')
+        # now do the move of all the tagged objects
+        self.widget.move('move', 0, dist)
+    
+    def PVT_click(self, event):
+        """Handle mouse clicks by kicking off possible drag'n'drop
+        processing"""
+        if self.widget.drop_callback:
+            if Tkdnd.dnd_start(self, event):
+                x1, y1, x2, y2=self.widget.bbox(self.symbol)
+                self.x_off=(x1-x2)/2
+                self.y_off=(y1-y2)/2
+        else:
+            # no callback, don't bother with drag'n'drop
+            self.widget.drag=0
+            self.dnd_end(None, None)
+
+#------------------------------------------------------------------------------
+class Tree(Canvas):
+    # do we have enough possible arguments?!?!?!
+    def __init__(self, master, root_id, root_label='',
+                 get_contents_callback=None, dist_x=15, dist_y=15,
+                 text_offset=10, line_flag=1, expanded_icon=None,
+                 collapsed_icon=None, regular_icon=None, plus_icon=None,
+                 minus_icon=None, node_class=Node, drop_callback=None,
+                 *args, **kw_args):
+        # pass args to superclass (new idiom from Python 2.2)
+        Canvas.__init__(self, master, *args, **kw_args)
+        
+        # this allows to subclass Node and pass our class in
+        self.node_class=node_class
+        # keep track of node bindings
+        self.bindings={}
+        # cheap mutex spinlock
+        self.spinlock=0
+        # flag to see if there's been any d&d dragging
+        self.drag=0
+        # default images (BASE64-encoded GIF files)
+        if expanded_icon == None:
+            self.expanded_icon=PhotoImage(
+                data='R0lGODlhEAANAKIAAAAAAMDAwICAgP//////ADAwMAAAAAAA' \
+                'ACH5BAEAAAEALAAAAAAQAA0AAAM6GCrM+jCIQamIbw6ybXNSx3GVB' \
+                'YRiygnA534Eq5UlO8jUqLYsquuy0+SXap1CxBHr+HoBjoGndDpNAAA7')
+        else:
+            self.expanded_icon=expanded_icon
+        if collapsed_icon == None:
+            self.collapsed_icon=PhotoImage(
+                data='R0lGODlhDwANAKIAAAAAAMDAwICAgP//////ADAwMAAAAAAA' \
+                'ACH5BAEAAAEALAAAAAAPAA0AAAMyGCHM+lAMMoeAT9Jtm5NDKI4Wo' \
+                'FXcJphhipanq7Kvu8b1dLc5tcuom2foAQQAyKRSmQAAOw==')
+        else:
+            self.collapsed_icon=collapsed_icon
+        if regular_icon == None:
+            self.regular_icon=PhotoImage(
+                data='R0lGODlhCwAOAJEAAAAAAICAgP///8DAwCH5BAEAAAMALAAA' \
+                'AAALAA4AAAIphA+jA+JuVgtUtMQePJlWCgSN9oSTV5lkKQpo2q5W+' \
+                'wbzuJrIHgw1WgAAOw==')
+        else:
+            self.regular_icon=regular_icon
+        if plus_icon == None:
+            self.plus_icon=PhotoImage(
+                data='R0lGODdhCQAJAPEAAAAAAH9/f////wAAACwAAAAACQAJAAAC' \
+                'FIyPoiu2sJyCyoF7W3hxz850CFIA\nADs=')
+        else:
+            self.plus_icon=plus_icon
+        if minus_icon == None:
+            self.minus_icon=PhotoImage(
+                data='R0lGODdhCQAJAPEAAAAAAH9/f////wAAACwAAAAACQAJAAAC' \
+                'EYyPoivG614LAlg7ZZbxoR8UADs=')
+        else:
+            self.minus_icon=minus_icon
+        # horizontal distance that subtrees are indented
+        self.dist_x=dist_x
+        # vertical distance between rows
+        self.dist_y=dist_y
+        # how far to offset text label
+        self.text_offset=text_offset
+        # flag controlling connecting line display
+        self.line_flag=line_flag
+        # called just before subtree expand/collapse
+        self.get_contents_callback=get_contents_callback
+        # called after drag'n'drop
+        self.drop_callback=drop_callback
+        # create root node to get the ball rolling
+        self.root=node_class(parent_node=None, label=root_label,
+                             id=root_id, expandable_flag=1,
+                             collapsed_icon=self.collapsed_icon,
+                             expanded_icon=self.expanded_icon,
+                             x=dist_x, y=dist_y, parent_widget=self)
+        # configure for scrollbar(s)
+        x1, y1, x2, y2=self.bbox('all') 
+        self.configure(scrollregion=(x1, y1, x2+5, y2+5))
+        # add a cursor
+        self.cursor_box=self.create_rectangle(0, 0, 0, 0)
+        self.move_cursor(self.root)
+        # make it easy to point to control
+        self.bind('<Enter>', self.PVT_mousefocus)
+        # totally arbitrary yet hopefully intuitive default keybindings
+        # stole 'em from ones used by microsoft tree control
+        # page-up/page-down
+        self.bind('<Next>', self.pagedown)
+        self.bind('<Prior>', self.pageup)
+        # arrow-up/arrow-down
+        self.bind('<Down>', self.next)
+        self.bind('<Up>', self.prev)
+        # arrow-left/arrow-right
+        self.bind('<Left>', self.ascend)
+        # (hold this down and you expand the entire tree)
+        self.bind('<Right>', self.descend)
+        # home/end
+        self.bind('<Home>', self.first)
+        self.bind('<End>', self.last)
+        # space bar
+        self.bind('<Key-space>', self.toggle)
+
+    # ----- PRIVATE METHODS (prefixed with "PVT_") -----
+    # these methods are subject to change, so please try not to use them
+    def PVT_mousefocus(self, event):
+        """Soak up event argument when moused-over"""
+        self.focus_set()
+        
+    # ----- PUBLIC METHODS -----
+    def tag_bind(self, tag, seq, *args, **kw_args):
+        """Keep track of callback bindings so we can delete them later. I
+        shouldn't have to do this!!!!"""
+        # pass args to superclass
+        func_id=apply(Canvas.tag_bind, (self, tag, seq)+args, kw_args)
+        # save references
+        self.bindings[tag]=self.bindings.get(tag, [])+[(seq, func_id)]
+
+    def add_list(self, list=None, name=None, id=None, flag=0,
+                 expanded_icon=None, collapsed_icon=None):
+        """Add node construction info to list"""
+        n=Struct()
+        n.name=name
+        n.id=id
+        n.flag=flag
+        if collapsed_icon:
+            n.collapsed_icon=collapsed_icon
+        else:
+            if flag:
+                # it's expandable, use closed folder icon
+                n.collapsed_icon=self.collapsed_icon
+            else:
+                # it's not expandable, use regular file icon
+                n.collapsed_icon=self.regular_icon
+        if flag:
+            if expanded_icon:
+                n.expanded_icon=expanded_icon
+            else:
+                n.expanded_icon=self.expanded_icon
+        else:
+            # not expandable, don't need an icon
+            n.expanded_icon=None
+        if list == None:
+            list=[]
+        list.append(n)
+        return list
+
+    def add_node(self, name=None, id=None, flag=0, expanded_icon=None,
+                 collapsed_icon=None):
+        """Add a node during get_contents_callback()"""
+        self.add_list(self.new_nodes, name, id, flag, expanded_icon,
+                      collapsed_icon)
+
+    def find_full_id(self, search):
+        """Search for a node"""
+        return self.root.PVT_find(search)
+    
+    def cursor_node(self, search):
+        """Return node under cursor"""
+        return self.pos
+        
+    def see(self, *items):
+        """Scroll (in a series of nudges) so items are visible"""
+        x1, y1, x2, y2=apply(self.bbox, items)
+        while x2 > self.canvasx(0)+self.winfo_width():
+            old=self.canvasx(0)
+            self.xview('scroll', 1, 'units')
+            # avoid endless loop if we can't scroll
+            if old == self.canvasx(0):
+                break
+        while y2 > self.canvasy(0)+self.winfo_height():
+            old=self.canvasy(0)
+            self.yview('scroll', 1, 'units')
+            if old == self.canvasy(0):
+                break
+        # done in this order to ensure upper-left of object is visible
+        while x1 < self.canvasx(0):
+            old=self.canvasx(0)
+            self.xview('scroll', -1, 'units')
+            if old == self.canvasx(0):
+                break
+        while y1 < self.canvasy(0):
+            old=self.canvasy(0)
+            self.yview('scroll', -1, 'units')
+            if old == self.canvasy(0):
+                break
+            
+    def move_cursor(self, node):
+        """Move cursor to node"""
+        self.pos=node
+        x1, y1, x2, y2=self.bbox(node.symbol, node.label)
+        self.coords(self.cursor_box, x1-1, y1-1, x2+1, y2+1)
+        self.see(node.symbol, node.label)
+    
+    def toggle(self, event=None):
+        """Expand/collapse subtree"""
+        self.pos.toggle_state()
+
+    def next(self, event=None):
+        """Move to next lower visible node"""
+        self.move_cursor(self.pos.next_visible())
+            
+    def prev(self, event=None):
+        """Move to next higher visible node"""
+        self.move_cursor(self.pos.prev_visible())
+
+    def ascend(self, event=None):
+        """Move to immediate parent"""
+        if self.pos.parent_node:
+            # move to parent
+            self.move_cursor(self.pos.parent_node)
+
+    def descend(self, event=None):
+        """Move right, expanding as we go"""
+        if self.pos.expandable_flag:
+            self.pos.expand()
+            if self.pos.child_nodes:
+                # move to first subnode
+                self.move_cursor(self.pos.child_nodes[0])
+                return
+        # if no subnodes, move to next sibling
+        self.next()
+
+    def first(self, event=None):
+        """Go to root node"""
+        # move to root node
+        self.move_cursor(self.root)
+
+    def last(self, event=None):
+        """Go to last visible node"""
+        # move to bottom-most node
+        self.move_cursor(self.root.PVT_last())
+
+    def pageup(self, event=None):
+        """Previous page"""
+        n=self.pos
+        j=self.winfo_height()/self.dist_y
+        for i in range(j-3):
+            n=n.prev_visible()
+        self.yview('scroll', -1, 'pages')
+        self.move_cursor(n)
+
+    def pagedown(self, event=None):
+        """Next page"""
+        n=self.pos
+        j=self.winfo_height()/self.dist_y
+        for i in range(j-3):
+            n=n.next_visible()
+        self.yview('scroll', 1, 'pages')
+        self.move_cursor(n)
+        
+    # ----- functions for drag'n'drop support -----
+    def where(self, event):
+        """Determine drag location in canvas coordinates. event.x & event.y
+        don't seem to be what we want."""
+        # where the corner of the canvas is relative to the screen:
+        x_org=self.winfo_rootx()
+        y_org=self.winfo_rooty()
+        # where the pointer is relative to the canvas widget,
+        # including scrolling
+        x=self.canvasx(event.x_root-x_org)
+        y=self.canvasy(event.y_root-y_org)
+        return x, y
+    
+    def dnd_accept(self, source, event):
+        """Accept dnd messages, i.e. we're a legit drop target, and we do
+        implement d&d functions."""
+        self.target=None
+        return self
+
+    def dnd_enter(self, source, event):
+        """Get ready to drag or drag has entered widget (create drag
+        object)"""
+        # this flag lets us know there's been drag motion
+        self.drag=1
+        x, y=self.where(event)
+        x1, y1, x2, y2=source.widget.bbox(source.symbol, source.label)
+        dx, dy=x2-x1, y2-y1
+        # create dragging icon
+        if source.expanded_flag:
+            self.dnd_symbol=self.create_image(x, y,
+                                              image=source.expanded_icon)
+        else:
+            self.dnd_symbol=self.create_image(x, y,
+                                              image=source.collapsed_icon)
+        self.dnd_label=self.create_text(x+self.text_offset, y, 
+                                        text=source.get_label(),
+                                        justify='left',
+                                        anchor='w')
+
+    def dnd_motion(self, source, event):
+        """Move drag icon"""
+        self.drag=1
+        x, y=self.where(event)
+        x1, y1, x2, y2=self.bbox(self.dnd_symbol, self.dnd_label)
+        self.move(self.dnd_symbol, x-x1+source.x_off, y-y1+source.y_off)
+        self.move(self.dnd_label, x-x1+source.x_off, y-y1+source.y_off)
+
+    def dnd_leave(self, source, event):
+        """Finish dragging or drag has left widget (destroy drag object)"""
+        self.delete(self.dnd_symbol)
+        self.delete(self.dnd_label)
+
+    def dnd_commit(self, source, event):
+        """Object has been dropped here"""
+        # call our own dnd_leave() to clean up
+        self.dnd_leave(source, event)
+        # process pending events to detect target node
+        # update_idletasks() doesn't do the trick if source & target are
+        # on  different widgets
+        self.update()
+        if not self.target:
+            # no target node
+            return
+        # we must update data structures based on the drop
+        if self.drop_callback:
+            try:
+                # called with dragged node and target node
+                # this is where a file manager would move the actual file
+                # it must also move the nodes around as it wishes
+                self.drop_callback(source, self.target)
+            except:
+                report_callback_exception()
+
+#------------------------------------------------------------------------------
+# the good 'ol test/demo code
+if __name__ == '__main__':
+    import os
+    import sys
+
+    # default routine to get contents of subtree
+    # supply this for a different type of app
+    # argument is the node object being expanded
+    # should call add_node()
+    def get_contents(node):
+        path=apply(os.path.join, node.full_id())
+        for filename in os.listdir(path):
+            full=os.path.join(path, filename)
+            name=filename
+            folder=0
+            if os.path.isdir(full):
+                # it's a directory
+                folder=1
+            elif not os.path.isfile(full):
+                # but it's not a file
+                name=name+' (special)'
+            if os.path.islink(full):
+                # it's a link
+                name=name+' (link to '+os.readlink(full)+')'
+            node.widget.add_node(name=name, id=filename, flag=folder)
+
+    root=Tk()
+    root.title(os.path.basename(sys.argv[0]))
+    tree=os.sep
+    if sys.platform == 'win32':
+        # we could call the root "My Computer" and mess with get_contents()
+        # to return "A:", "B:", "C:", ... etc. as it's children, but that
+        # would just be terminally cute and I'd have to shoot myself
+        tree='C:'+os.sep
+
+    # create the control
+    t=Tree(master=root,
+           root_id=tree,
+           root_label=tree,
+           get_contents_callback=get_contents,
+           width=300)
+    t.grid(row=0, column=0, sticky='nsew')
+
+    # make expandable
+    root.grid_rowconfigure(0, weight=1)
+    root.grid_columnconfigure(0, weight=1)
+
+    # add scrollbars
+    sb=Scrollbar(root)
+    sb.grid(row=0, column=1, sticky='ns')
+    t.configure(yscrollcommand=sb.set)
+    sb.configure(command=t.yview)
+
+    sb=Scrollbar(root, orient=HORIZONTAL)
+    sb.grid(row=1, column=0, sticky='ew')
+    t.configure(xscrollcommand=sb.set)
+    sb.configure(command=t.xview)
+
+    # must get focus so keys work for demo
+    t.focus_set()
+
+    # we could do without this, but it's nice and friendly to have
+    Button(root, text='Quit', command=root.quit).grid(row=2, column=0,
+                                                      columnspan=2)
+
+    # expand out the root
+    t.root.expand()
+    
+    root.mainloop()

Added: py/branch/py-collect/test/tkinter/utils.py
==============================================================================
--- (empty file)
+++ py/branch/py-collect/test/tkinter/utils.py	Mon Apr  4 17:32:41 2005
@@ -0,0 +1,247 @@
+import types, string, sys
+import py
+from py.__impl__.test.report.text import out
+from py.__impl__.test.terminal import TerminalDriver
+
+class Null:
+    """ Null objects always and reliably "do nothing." """
+
+    def __init__(self, *args, **kwargs): pass
+    def __call__(self, *args, **kwargs): return self
+    def __repr__(self): return "Null()"
+    def __str__(self): return repr(self) + ' with id:' + str(id(self))
+    def __nonzero__(self): return 0
+
+    def __getattr__(self, name): return self
+    def __setattr__(self, name, value): return self
+    def __delattr__(self, name): return self
+
+    
+_NotExecuted = 'NotExecuted'
+_Passed = 'Passed'
+_Failed = 'Failed'
+_Skipped = 'Skipped'
+_ExceptionFailure = 'ExceptionFailure'
+
+class Status(object):
+
+    @classmethod
+    def NotExecuted(cls):
+        return cls('NotExecuted')
+    @classmethod
+    def Passed(cls):
+        return cls('Passed')
+    @classmethod
+    def Failed(cls):
+        return cls('Failed')
+    @classmethod
+    def Skipped(cls):
+        return cls('Skipped')
+    @classmethod
+    def ExceptionFailure(cls):
+        return cls(_ExceptionFailure)
+
+    ordered_list = [_NotExecuted,
+                    _Passed,
+                    _Skipped,
+                    _Failed,
+                    _ExceptionFailure]
+        
+    namemap = {
+        py.test.Item.Passed: _Passed,
+        py.test.Item.Skipped: _Skipped,
+        py.test.Item.Failed: _Failed,
+        py.test.Item.ExceptionFailure: _ExceptionFailure }
+    
+    def __init__(self, outcome_or_name = ''):
+        self.str = _NotExecuted
+        if isinstance(outcome_or_name, py.test.Collector.Outcome):
+            # hack
+            if isinstance(outcome_or_name, py.test.Item.ExceptionFailure):
+                self.str = self.namemap[py.test.Item.ExceptionFailure]
+            else:
+                for restype, name in self.namemap.items():
+                    if isinstance(outcome_or_name, restype):
+                        self.str = name
+        else:
+            if str(outcome_or_name) in self.ordered_list:
+                self.str = str(outcome_or_name)
+
+    def __repr__(self):
+        return 'Status("%s")' % self.str
+
+    def __str__(self):
+        return self.str
+
+    def update(self, status):
+        name_int_map = dict(
+            py.std.itertools.izip(self.ordered_list,
+                                  py.std.itertools.count()))
+        self.str = self.ordered_list[max([name_int_map[i] for i in (str(status), self.str)])]
+        
+    def __eq__(self, other):
+        return self.str == other.str
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+class OutBuffer(out.Out):
+
+    def __init__(self, fullwidth = 80 -1):
+        self.output = []
+        self.fullwidth = fullwidth
+
+    def line(self, s= ''):
+        self.output.append(str(s) + '\n')
+
+    def write(self, s):
+        self.output.append(str(s))
+
+    def getoutput(self):
+        return ''.join(self.output)
+
+    def rewrite(self, s=''):
+        self.write(s)
+
+   
+
+
+class TestReport(object):
+
+    template = {'time' : 0,
+                'label': 'Root',
+                'id': 'Root',
+                'full_id': ['Root'],
+                'status': Status.NotExecuted(),
+                'report': 'NoReport',
+                'error_report': '',
+                'finished': False,
+                'restart_params': None, # ('',('',))
+                }
+    @classmethod
+    def fromChannel(cls, kwdict):
+        if 'status' in kwdict:
+            kwdict['status'] = Status(kwdict['status'])
+        return cls(**kwdict)
+    
+
+    def __init__(self, **kwargs):
+        # copy status -> deepcopy
+        kwdict = py.std.copy.deepcopy(self.template)
+        kwdict.update(kwargs)
+        for key, value in kwdict.iteritems():
+            setattr(self, key, value)
+
+    def start(self, collector):
+        self.full_id = collector.listnames()
+        self.id = collector.name
+        if collector.getpathlineno(): # save for Null() in test_utils.py
+            fspath, lineno = collector.getpathlineno()
+            if lineno != sys.maxint:
+                str_append = ' [%s:%s]' % (fspath.basename, lineno)
+            else:
+                str_append = ' [%s]' % fspath.basename
+            self.label = collector.name + str_append
+
+        self.settime()
+        self.restart_params = (str(collector.listchain()[0].fspath),
+                               collector.listnames())
+        self.status = Status.NotExecuted()
+
+    def finish(self, collector, res, option = Null()):
+        self.settime()
+        if collector.getpathlineno(): # save for Null() in test_utils.py
+            fspath, lineno = collector.getpathlineno()
+            if lineno != sys.maxint:
+                str_append = ' [%s:%s] %0.2fsecs' % (fspath.basename, lineno, self.time)
+            else:
+                str_append = ' [%s] %0.2fsecs' % (fspath.basename, self.time)
+            self.label = collector.name + str_append
+        if res:
+            if Status(res) in (Status.Failed(), Status.ExceptionFailure()):
+                self.error_report = self.report_failed(option, collector, res)
+            elif Status(res) ==  Status.Skipped():
+                self.error_report = self.report_skipped(option, collector, res)
+            self.status.update(Status(res))
+        self.finished = True
+
+    def report_failed(self, option, item, res):
+        #XXX hack abuse of TerminalDriver
+        terminal = TerminalDriver(option)
+        out = OutBuffer()
+        terminal.out = out
+        terminal.repr_failure(item, res)
+        return out.getoutput()
+
+    def report_skipped(self,option, item, res):
+        texts = {}
+        tbindex = getattr(res, 'tbindex', -1)
+        raisingtb = res.excinfo.traceback[tbindex] 
+        fn = raisingtb.frame.code.path
+        lineno = raisingtb.lineno
+        d = texts.setdefault(res.excinfo.exconly(), {})
+        d[(fn,lineno)] = res
+        out = OutBuffer()
+        out.sep('_', 'reasons for skipped tests')
+        for text, dict in texts.items():
+            for (fn, lineno), res in dict.items(): 
+                out.line('Skipped in %s:%d' %(fn,lineno))
+            out.line("reason: %s" % text) 
+
+        return out.getoutput()
+        
+    def settime(self):
+        self.time = py.std.time.time() - self.time 
+
+    def toChannel(self):
+        ret = self.template.copy()
+        for key in ret.keys():
+            ret[key] = getattr(self, key, self.template[key])
+        ret['status'] = str(ret['status'])
+        return ret
+
+    def __str__(self):
+        return str(self.toChannel())
+
+    def __repr__(self):
+        return str(self)
+
+    def copy(self, **kwargs):
+        channel_dict = self.toChannel()
+        channel_dict.update(kwargs)
+        return TestReport.fromChannel(channel_dict)
+
+class TestFileWatcher:
+
+    def __init__(self, *paths):
+        self.paths = [py.path.local(path) for path in paths]
+        self.watchdict = dict()
+
+    def file_information(self, path):
+        try:
+            return path.stat().st_ctime
+        except:
+            return None
+
+    def check_files(self):
+        fil = py.path.checker(fnmatch='*.py')
+        rec = py.path.checker(dotfile=0)
+
+        files = []
+        for path in self.paths:
+            if path.check(file=1):
+                files.append(path)
+            else:
+                files.extend(path.visit(fil, rec))
+        newdict = dict(zip(files, [self.file_information(p) for p in files]))
+        files_deleted = [f for f in self.watchdict.keys() if not newdict.has_key(f)]
+        files_new = [f for f in newdict.keys() if not self.watchdict.has_key(f)]
+        files_changed = [f for f in newdict.keys() if self.watchdict.has_key(f) and newdict[f]!= self.watchdict[f]]
+        files_changed = files_new + files_changed
+
+        self.watchdict = newdict
+        return files_changed, files_deleted
+
+    def changed(self):
+        changed, deleted = self.check_files()
+        return changed != [] or deleted != []



More information about the pytest-commit mailing list