[pygobject] pygtkcompat: Add pygtk compatible GenericTreeModel implementation



commit a10fb7216de57046d1ecacb73dd032eaadcbad09
Author: Simon Feltman <s feltman gmail com>
Date:   Wed Aug 29 03:46:23 2012 -0700

    pygtkcompat: Add pygtk compatible GenericTreeModel implementation
    
    Add Python implementation of the GenericTreeModel that was
    available in pygtk. The implementation attempts a better job
    than the original at ref counting by guaranteeing no leaks
    upon deletion of the model itself. Or by using the extra "node"
    argument to the row_deleted signal. The model is available in
    the pygtkcompat package directly as
    pygtkcompat.generictreemodel.GenericTreeModel or with as
    gtk.GenericTreeModel when pygtkcompat.enable_gtk() is set.
    
    Add file list and tree demos making use of GenericTreeModel
    to gtk-demo.
    
    Auto-expand gtk-demo app tree to give a better overview of
    the demos available.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=682933

 .../gtk-demo/demos/Tree View/treemodel_filelist.py |  234 +++++++++++
 .../gtk-demo/demos/Tree View/treemodel_filetree.py |  279 +++++++++++++
 demos/gtk-demo/gtk-demo.py                         |    2 +-
 gi/pygtkcompat.py                                  |    2 +-
 pygtkcompat/Makefile.am                            |    1 +
 pygtkcompat/generictreemodel.py                    |  420 ++++++++++++++++++++
 pygtkcompat/pygtkcompat.py                         |    3 +
 tests/Makefile.am                                  |    1 +
 tests/test_generictreemodel.py                     |  406 +++++++++++++++++++
 9 files changed, 1346 insertions(+), 2 deletions(-)
---
diff --git a/demos/gtk-demo/demos/Tree View/treemodel_filelist.py b/demos/gtk-demo/demos/Tree 
View/treemodel_filelist.py
new file mode 100644
index 0000000..96c2620
--- /dev/null
+++ b/demos/gtk-demo/demos/Tree View/treemodel_filelist.py      
@@ -0,0 +1,234 @@
+#!/usr/bin/env python
+
+title = "File List (GenericTreeModel)"
+description = """
+This is a file list demo which makes use of the GenericTreeModel python
+implementation of the Gtk.TreeModel interface. This demo shows what methods
+need to be overridden to provide a valid TreeModel to a TreeView.
+"""
+
+import os
+import stat
+import time
+
+import pygtkcompat
+pygtkcompat.enable()
+pygtkcompat.enable_gtk('3.0')
+
+import gtk
+
+
+folderxpm = [
+    "17 16 7 1",
+    "  c #000000",
+    ". c #808000",
+    "X c yellow",
+    "o c #808080",
+    "O c #c0c0c0",
+    "+ c white",
+    "@ c None",
+    "@@@@@@@@@@@@@@@@@",
+    "@@@@@@@@@@@@@@@@@",
+    "@@+XXXX.@@@@@@@@@",
+    "@+OOOOOO.@@@@@@@@",
+    "@+OXOXOXOXOXOXO. ",
+    "@+XOXOXOXOXOXOX. ",
+    "@+OXOXOXOXOXOXO. ",
+    "@+XOXOXOXOXOXOX. ",
+    "@+OXOXOXOXOXOXO. ",
+    "@+XOXOXOXOXOXOX. ",
+    "@+OXOXOXOXOXOXO. ",
+    "@+XOXOXOXOXOXOX. ",
+    "@+OOOOOOOOOOOOO. ",
+    "@                ",
+    "@@@@@@@@@@@@@@@@@",
+    "@@@@@@@@@@@@@@@@@"
+    ]
+folderpb = gtk.gdk.pixbuf_new_from_xpm_data(folderxpm)
+
+filexpm = [
+    "12 12 3 1",
+    "  c #000000",
+    ". c #ffff04",
+    "X c #b2c0dc",
+    "X        XXX",
+    "X ...... XXX",
+    "X ......   X",
+    "X .    ... X",
+    "X ........ X",
+    "X .   .... X",
+    "X ........ X",
+    "X .     .. X",
+    "X ........ X",
+    "X .     .. X",
+    "X ........ X",
+    "X          X"
+    ]
+filepb = gtk.gdk.pixbuf_new_from_xpm_data(filexpm)
+
+
+class FileListModel(gtk.GenericTreeModel):
+    __gtype_name__ = 'DemoFileListModel'
+
+    column_types = (gtk.gdk.Pixbuf, str, int, str, str)
+    column_names = ['Name', 'Size', 'Mode', 'Last Changed']
+
+    def __init__(self, dname=None):
+        gtk.GenericTreeModel.__init__(self)
+        self._sort_column_id = 0
+        self._sort_order = gtk.SORT_ASCENDING
+
+        if not dname:
+            self.dirname = os.path.expanduser('~')
+        else:
+            self.dirname = os.path.abspath(dname)
+        self.files = ['..'] + [f for f in os.listdir(self.dirname)]
+        return
+
+    def get_pathname(self, path):
+        filename = self.files[path[0]]
+        return os.path.join(self.dirname, filename)
+
+    def is_folder(self, path):
+        filename = self.files[path[0]]
+        pathname = os.path.join(self.dirname, filename)
+        filestat = os.stat(pathname)
+        if stat.S_ISDIR(filestat.st_mode):
+            return True
+        return False
+
+    def get_column_names(self):
+        return self.column_names[:]
+
+    #
+    # GenericTreeModel Implementation
+    #
+    def on_get_flags(self):
+        return 0  # gtk.TREE_MODEL_ITERS_PERSIST
+
+    def on_get_n_columns(self):
+        return len(self.column_types)
+
+    def on_get_column_type(self, n):
+        return self.column_types[n]
+
+    def on_get_iter(self, path):
+        return self.files[path[0]]
+
+    def on_get_path(self, rowref):
+        return self.files.index(rowref)
+
+    def on_get_value(self, rowref, column):
+        fname = os.path.join(self.dirname, rowref)
+        try:
+            filestat = os.stat(fname)
+        except OSError:
+            return None
+        mode = filestat.st_mode
+        if column is 0:
+            if stat.S_ISDIR(mode):
+                return folderpb
+            else:
+                return filepb
+        elif column is 1:
+            return rowref
+        elif column is 2:
+            return filestat.st_size
+        elif column is 3:
+            return oct(stat.S_IMODE(mode))
+        return time.ctime(filestat.st_mtime)
+
+    def on_iter_next(self, rowref):
+        try:
+            i = self.files.index(rowref) + 1
+            return self.files[i]
+        except IndexError:
+            return None
+
+    def on_iter_children(self, rowref):
+        if rowref:
+            return None
+        return self.files[0]
+
+    def on_iter_has_child(self, rowref):
+        return False
+
+    def on_iter_n_children(self, rowref):
+        if rowref:
+            return 0
+        return len(self.files)
+
+    def on_iter_nth_child(self, rowref, n):
+        if rowref:
+            return None
+        try:
+            return self.files[n]
+        except IndexError:
+            return None
+
+    def on_iter_parent(child):
+        return None
+
+
+class GenericTreeModelExample:
+    def delete_event(self, widget, event, data=None):
+        gtk.main_quit()
+        return False
+
+    def __init__(self):
+        # Create a new window
+        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+
+        self.window.set_size_request(300, 200)
+
+        self.window.connect("delete_event", self.delete_event)
+
+        self.listmodel = FileListModel()
+
+        # create the TreeView
+        self.treeview = gtk.TreeView()
+
+        self.tvcolumns = []
+
+        # create the TreeViewColumns to display the data
+        for n, name in enumerate(self.listmodel.get_column_names()):
+            if n == 0:
+                cellpb = gtk.CellRendererPixbuf()
+                col = gtk.TreeViewColumn(name, cellpb, pixbuf=0)
+                cell = gtk.CellRendererText()
+                col.pack_start(cell, False)
+                col.add_attribute(cell, 'text', 1)
+            else:
+                cell = gtk.CellRendererText()
+                col = gtk.TreeViewColumn(name, cell, text=n + 1)
+            if n == 1:
+                cell.set_property('xalign', 1.0)
+
+            self.treeview.append_column(col)
+
+        self.treeview.connect('row-activated', self.open_file)
+
+        self.scrolledwindow = gtk.ScrolledWindow()
+        self.scrolledwindow.add(self.treeview)
+        self.window.add(self.scrolledwindow)
+        self.treeview.set_model(self.listmodel)
+        self.window.set_title(self.listmodel.dirname)
+        self.window.show_all()
+
+    def open_file(self, treeview, path, column):
+        model = treeview.get_model()
+        if model.is_folder(path):
+            pathname = model.get_pathname(path)
+            new_model = FileListModel(pathname)
+            self.window.set_title(new_model.dirname)
+            treeview.set_model(new_model)
+        return
+
+
+def main(demoapp=None):
+    demo = GenericTreeModelExample()
+    demo
+    gtk.main()
+
+if __name__ == "__main__":
+    main()
diff --git a/demos/gtk-demo/demos/Tree View/treemodel_filetree.py b/demos/gtk-demo/demos/Tree 
View/treemodel_filetree.py
new file mode 100644
index 0000000..3179de2
--- /dev/null
+++ b/demos/gtk-demo/demos/Tree View/treemodel_filetree.py      
@@ -0,0 +1,279 @@
+#!/usr/bin/env python
+
+title = "File Tree (GenericTreeModel)"
+description = """
+This is a file list demo which makes use of the GenericTreeModel python
+implementation of the Gtk.TreeModel interface. This demo shows what methods
+need to be overridden to provide a valid TreeModel to a TreeView.
+"""
+
+import os
+import stat
+import time
+from collections import OrderedDict
+
+import pygtkcompat
+pygtkcompat.enable_gtk('3.0')
+
+import gtk
+
+
+folderxpm = [
+    "17 16 7 1",
+    "  c #000000",
+    ". c #808000",
+    "X c yellow",
+    "o c #808080",
+    "O c #c0c0c0",
+    "+ c white",
+    "@ c None",
+    "@@@@@@@@@@@@@@@@@",
+    "@@@@@@@@@@@@@@@@@",
+    "@@+XXXX.@@@@@@@@@",
+    "@+OOOOOO.@@@@@@@@",
+    "@+OXOXOXOXOXOXO. ",
+    "@+XOXOXOXOXOXOX. ",
+    "@+OXOXOXOXOXOXO. ",
+    "@+XOXOXOXOXOXOX. ",
+    "@+OXOXOXOXOXOXO. ",
+    "@+XOXOXOXOXOXOX. ",
+    "@+OXOXOXOXOXOXO. ",
+    "@+XOXOXOXOXOXOX. ",
+    "@+OOOOOOOOOOOOO. ",
+    "@                ",
+    "@@@@@@@@@@@@@@@@@",
+    "@@@@@@@@@@@@@@@@@"
+    ]
+folderpb = gtk.gdk.pixbuf_new_from_xpm_data(folderxpm)
+
+filexpm = [
+    "12 12 3 1",
+    "  c #000000",
+    ". c #ffff04",
+    "X c #b2c0dc",
+    "X        XXX",
+    "X ...... XXX",
+    "X ......   X",
+    "X .    ... X",
+    "X ........ X",
+    "X .   .... X",
+    "X ........ X",
+    "X .     .. X",
+    "X ........ X",
+    "X .     .. X",
+    "X ........ X",
+    "X          X"
+    ]
+filepb = gtk.gdk.pixbuf_new_from_xpm_data(filexpm)
+
+
+class FileTreeModel(gtk.GenericTreeModel):
+    __gtype_name__ = 'DemoFileTreeModel'
+
+    column_types = (gtk.gdk.Pixbuf, str, int, str, str)
+    column_names = ['Name', 'Size', 'Mode', 'Last Changed']
+
+    def __init__(self, dname=None):
+        gtk.GenericTreeModel.__init__(self)
+        if not dname:
+            self.dirname = os.path.expanduser('~')
+        else:
+            self.dirname = os.path.abspath(dname)
+        self.files = self.build_file_dict(self.dirname)
+        return
+
+    def build_file_dict(self, dirname):
+        """
+        :Returns:
+            A dictionary containing the files in the given dirname keyed by filename.
+            If the child filename is a sub-directory, the dict value is a dict.
+            Otherwise it will be None.
+        """
+        d = OrderedDict()
+        for fname in os.listdir(dirname):
+            try:
+                filestat = os.stat(os.path.join(dirname, fname))
+            except OSError:
+                d[fname] = None
+            else:
+                d[fname] = OrderedDict() if stat.S_ISDIR(filestat.st_mode) else None
+
+        return d
+
+    def get_node_from_treepath(self, path):
+        """
+        :Returns:
+            The node stored at the given tree path in local storage.
+        """
+        # TreePaths are a series of integer indices so just iterate through them
+        # and index values by each integer since we are using an OrderedDict
+        if path is None:
+            path = []
+        node = self.files
+        for index in path:
+            node = list(node.values())[index]
+        return node
+
+    def get_node_from_filepath(self, filepath):
+        """
+        :Returns:
+            The node stored at the given file path in local storage.
+        """
+        if not filepath:
+            return self.files
+        node = self.files
+        for key in filepath.split(os.path.sep):
+            node = node[key]
+        return node
+
+    def get_column_names(self):
+        return self.column_names[:]
+
+    #
+    # GenericTreeModel Implementation
+    #
+
+    def on_get_flags(self):
+        return 0
+
+    def on_get_n_columns(self):
+        return len(self.column_types)
+
+    def on_get_column_type(self, n):
+        return self.column_types[n]
+
+    def on_get_path(self, relpath):
+        path = []
+        node = self.files
+        for key in relpath.split(os.path.sep):
+            path.append(list(node.keys()).index(key))
+            node = node[key]
+        return path
+
+    def on_get_value(self, relpath, column):
+        fname = os.path.join(self.dirname, relpath)
+        try:
+            filestat = os.stat(fname)
+        except OSError:
+            return None
+        mode = filestat.st_mode
+        if column is 0:
+            if stat.S_ISDIR(mode):
+                return folderpb
+            else:
+                return filepb
+        elif column is 1:
+            return os.path.basename(relpath)
+        elif column is 2:
+            return filestat.st_size
+        elif column is 3:
+            return oct(stat.S_IMODE(mode))
+        return time.ctime(filestat.st_mtime)
+
+    def on_get_iter(self, path):
+        filepath = ''
+        value = self.files
+        for index in path:
+            filepath = os.path.join(filepath, list(value.keys())[index])
+            value = list(value.values())[index]
+        return filepath
+
+    def on_iter_next(self, filepath):
+        parent_path, child_path = os.path.split(filepath)
+        parent = self.get_node_from_filepath(parent_path)
+
+        # Index of filepath within its parents child list
+        sibling_names = list(parent.keys())
+        index = sibling_names.index(child_path)
+        try:
+            return os.path.join(parent_path, sibling_names[index + 1])
+        except IndexError:
+            return None
+
+    def on_iter_children(self, filepath):
+        if filepath:
+            children = list(self.get_node_from_filepath(filepath).keys())
+            if children:
+                return os.path.join(filepath, children[0])
+        elif self.files:
+            return list(self.files.keys())[0]
+
+        return None
+
+    def on_iter_has_child(self, filepath):
+        return bool(self.get_node_from_filepath(filepath))
+
+    def on_iter_n_children(self, filepath):
+        return len(self.get_node_from_filepath(filepath))
+
+    def on_iter_nth_child(self, filepath, n):
+        try:
+            child = list(self.get_node_from_filepath(filepath).keys())[n]
+            if filepath:
+                return os.path.join(filepath, child)
+            else:
+                return child
+        except IndexError:
+            return None
+
+    def on_iter_parent(self, filepath):
+        return os.path.dirname(filepath)
+
+    def on_ref_node(self, filepath):
+        value = self.get_node_from_filepath(filepath)
+        if value is not None:
+            value.update(self.build_file_dict(os.path.join(self.dirname, filepath)))
+
+    def on_unref_node(self, filepath):
+        pass
+
+
+class GenericTreeModelExample:
+    def delete_event(self, widget, event, data=None):
+        gtk.main_quit()
+        return False
+
+    def __init__(self):
+        # Create a new window
+        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+        self.window.set_size_request(300, 200)
+        self.window.connect("delete_event", self.delete_event)
+
+        self.listmodel = FileTreeModel()
+
+        # create the TreeView
+        self.treeview = gtk.TreeView()
+
+        # create the TreeViewColumns to display the data
+        column_names = self.listmodel.get_column_names()
+        self.tvcolumn = [None] * len(column_names)
+        cellpb = gtk.CellRendererPixbuf()
+        self.tvcolumn[0] = gtk.TreeViewColumn(column_names[0],
+                                              cellpb, pixbuf=0)
+        cell = gtk.CellRendererText()
+        self.tvcolumn[0].pack_start(cell, False)
+        self.tvcolumn[0].add_attribute(cell, 'text', 1)
+        self.treeview.append_column(self.tvcolumn[0])
+        for n in range(1, len(column_names)):
+            cell = gtk.CellRendererText()
+            if n == 1:
+                cell.set_property('xalign', 1.0)
+            self.tvcolumn[n] = gtk.TreeViewColumn(column_names[n],
+                                                  cell, text=n + 1)
+            self.treeview.append_column(self.tvcolumn[n])
+
+        self.scrolledwindow = gtk.ScrolledWindow()
+        self.scrolledwindow.add(self.treeview)
+        self.window.add(self.scrolledwindow)
+        self.treeview.set_model(self.listmodel)
+        self.window.set_title(self.listmodel.dirname)
+        self.window.show_all()
+
+
+def main(demoapp=None):
+    demo = GenericTreeModelExample()
+    demo
+    gtk.main()
+
+if __name__ == "__main__":
+    main()
diff --git a/demos/gtk-demo/gtk-demo.py b/demos/gtk-demo/gtk-demo.py
index 6de2e30..eeb0553 100755
--- a/demos/gtk-demo/gtk-demo.py
+++ b/demos/gtk-demo/gtk-demo.py
@@ -277,7 +277,7 @@ class GtkDemoWindow(Gtk.Window):
 
         tree_view.append_column(column)
 
-        tree_view.collapse_all()
+        tree_view.expand_all()
         tree_view.set_headers_visible(False)
         scrolled_window = Gtk.ScrolledWindow(hadjustment=None,
                                              vadjustment=None)
diff --git a/gi/pygtkcompat.py b/gi/pygtkcompat.py
index 91b5cc1..9a80eef 100644
--- a/gi/pygtkcompat.py
+++ b/gi/pygtkcompat.py
@@ -2,7 +2,7 @@ import warnings
 from gi import PyGIDeprecationWarning
 
 warnings.warn('gi.pygtkcompat is being deprecated in favor of using "pygtkcompat" directly.',
-              PyGIDeprecationWarning, stacklevel=2)
+              PyGIDeprecationWarning)
 
 # pyflakes.ignore
 from pygtkcompat import (enable,
diff --git a/pygtkcompat/Makefile.am b/pygtkcompat/Makefile.am
index 6a73cb4..914b3e2 100644
--- a/pygtkcompat/Makefile.am
+++ b/pygtkcompat/Makefile.am
@@ -2,6 +2,7 @@ pygtkcompatdir = $(pyexecdir)/pygtkcompat
 
 pygtkcompat_PYTHON = \
        __init__.py \
+       generictreemodel.py \
        pygtkcompat.py
 
 # if we build in a separate tree, we need to symlink the *.py files from the
diff --git a/pygtkcompat/generictreemodel.py b/pygtkcompat/generictreemodel.py
new file mode 100644
index 0000000..ad67d90
--- /dev/null
+++ b/pygtkcompat/generictreemodel.py
@@ -0,0 +1,420 @@
+# -*- Mode: Python; py-indent-offset: 4 -*-
+# generictreemodel - GenericTreeModel implementation for pygtk compatibility.
+# Copyright (C) 2013 Simon Feltman
+#
+#   generictreemodel.py: GenericTreeModel implementation for pygtk compatibility
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+
+# System
+import sys
+import random
+import collections
+import ctypes
+
+# GObject
+from gi.repository import GObject
+from gi.repository import Gtk
+
+
+class _CTreeIter(ctypes.Structure):
+    _fields_ = [('stamp', ctypes.c_int),
+                ('user_data', ctypes.c_void_p),
+                ('user_data2', ctypes.c_void_p),
+                ('user_data3', ctypes.c_void_p)]
+
+    @classmethod
+    def from_iter(cls, iter):
+        offset = sys.getsizeof(object())  # size of PyObject_HEAD
+        return ctypes.POINTER(cls).from_address(id(iter) + offset)
+
+
+def _get_user_data_as_pyobject(iter):
+    citer = _CTreeIter.from_iter(iter)
+    return ctypes.cast(citer.contents.user_data, ctypes.py_object).value
+
+
+def handle_exception(default_return):
+    """Returns a function which can act as a decorator for wrapping exceptions and
+    returning "default_return" upon an exception being thrown.
+
+    This is used to wrap Gtk.TreeModel "do_" method implementations so we can return
+    a proper value from the override upon an exception occurring with client code
+    implemented by the "on_" methods.
+    """
+    def decorator(func):
+        def wrapped_func(*args, **kargs):
+            try:
+                return func(*args, **kargs)
+            except:
+                # Use excepthook directly to avoid any printing to the screen
+                # if someone installed an except hook.
+                sys.excepthook(*sys.exc_info())
+            return default_return
+        return wrapped_func
+    return decorator
+
+
+class GenericTreeModel(GObject.GObject, Gtk.TreeModel):
+    """A base implementation of a Gtk.TreeModel for python.
+
+    The GenericTreeModel eases implementing the Gtk.TreeModel interface in Python.
+    The class can be subclassed to provide a TreeModel implementation which works
+    directly with Python objects instead of iterators.
+
+    All of the on_* methods should be overridden by subclasses to provide the
+    underlying implementation a way to access custom model data. For the purposes of
+    this API, all custom model data supplied or handed back through the overridable
+    API will use the argument names: node, parent, and child in regards to user data
+    python objects.
+
+    The create_tree_iter, set_user_data, invalidate_iters, iter_is_valid methods are
+    available to help manage Gtk.TreeIter objects and their Python object references.
+
+    GenericTreeModel manages a pool of user data nodes that have been used with iters.
+    This pool stores a references to user data nodes as a dictionary value with the
+    key being the integer id of the data. This id is what the Gtk.TreeIter objects
+    use to reference data in the pool.
+    References will be removed from the pool when the model is deleted or explicitly
+    by using the optional "node" argument to the "row_deleted" method when notifying
+    the model of row deletion.
+    """
+
+    leak_references = GObject.Property(default=True, type=bool,
+                                       blurb="If True, strong references to user data attached to iters are "
+                                       "stored in a dictionary pool (default). Otherwise the user data is "
+                                       "stored as a raw pointer to a python object without a reference.")
+
+    #
+    # Methods
+    #
+    def __init__(self):
+        """Initialize. Make sure to call this from derived classes if overridden."""
+        super(GenericTreeModel, self).__init__()
+        self.stamp = 0
+
+        #: Dictionary of (id(user_data): user_data), used when leak-refernces=False
+        self._held_refs = dict()
+
+        # Set initial stamp
+        self.invalidate_iters()
+
+    def iter_depth_first(self):
+        """Depth-first iteration of the entire TreeModel yielding the python nodes."""
+        stack = collections.deque([None])
+        while stack:
+            it = stack.popleft()
+            if it is not None:
+                yield self.get_user_data(it)
+            children = [self.iter_nth_child(it, i) for i in range(self.iter_n_children(it))]
+            stack.extendleft(reversed(children))
+
+    def invalidate_iter(self, iter):
+        """Clear user data and its reference from the iter and this model."""
+        iter.stamp = 0
+        if iter.user_data:
+            if iter.user_data in self._held_refs:
+                del self._held_refs[iter.user_data]
+            iter.user_data = None
+
+    def invalidate_iters(self):
+        """
+        This method invalidates all TreeIter objects associated with this custom tree model
+        and frees their locally pooled references.
+        """
+        self.stamp = random.randint(-2147483648, 2147483647)
+        self._held_refs.clear()
+
+    def iter_is_valid(self, iter):
+        """
+        :Returns:
+            True if the gtk.TreeIter specified by iter is valid for the custom tree model.
+        """
+        return iter.stamp == self.stamp
+
+    def get_user_data(self, iter):
+        """Get the user_data associated with the given TreeIter.
+
+        GenericTreeModel stores arbitrary Python objects mapped to instances of Gtk.TreeIter.
+        This method allows to retrieve the Python object held by the given iterator.
+        """
+        if self.leak_references:
+            return self._held_refs[iter.user_data]
+        else:
+            return _get_user_data_as_pyobject(iter)
+
+    def set_user_data(self, iter, user_data):
+        """Applies user_data and stamp to the given iter.
+
+        If the models "leak_references" property is set, a reference to the
+        user_data is stored with the model to ensure we don't run into bad
+        memory problems with the TreeIter.
+        """
+        iter.user_data = id(user_data)
+
+        if user_data is None:
+            self.invalidate_iter(iter)
+        else:
+            iter.stamp = self.stamp
+            if self.leak_references:
+                self._held_refs[iter.user_data] = user_data
+
+    def create_tree_iter(self, user_data):
+        """Create a Gtk.TreeIter instance with the given user_data specific for this model.
+
+        Use this method to create Gtk.TreeIter instance instead of directly calling
+        Gtk.Treeiter(), this will ensure proper reference managment of wrapped used_data.
+        """
+        iter = Gtk.TreeIter()
+        self.set_user_data(iter, user_data)
+        return iter
+
+    def _create_tree_iter(self, data):
+        """Internal creation of a (bool, TreeIter) pair for returning directly
+        back to the view interfacing with this model."""
+        if data is None:
+            return (False, None)
+        else:
+            it = self.create_tree_iter(data)
+            return (True, it)
+
+    def row_deleted(self, path, node=None):
+        """Notify the model a row has been deleted.
+
+        Use the node parameter to ensure the user_data reference associated
+        with the path is properly freed by this model.
+
+        :Parameters:
+            path : Gtk.TreePath
+                Path to the row that has been deleted.
+            node : object
+                Python object used as the node returned from "on_get_iter". This is
+                optional but ensures the model will not leak references to this object.
+        """
+        super(GenericTreeModel, self).row_deleted(path)
+        node_id = id(node)
+        if node_id in self._held_refs:
+            del self._held_refs[node_id]
+
+    #
+    # GtkTreeModel Interface Implementation
+    #
+    @handle_exception(0)
+    def do_get_flags(self):
+        """Internal method."""
+        return self.on_get_flags()
+
+    @handle_exception(0)
+    def do_get_n_columns(self):
+        """Internal method."""
+        return self.on_get_n_columns()
+
+    @handle_exception((False, None))
+    def do_get_column_type(self, index):
+        """Internal method."""
+        return self.on_get_column_type(index)
+
+    @handle_exception((False, None))
+    def do_get_iter(self, path):
+        """Internal method."""
+        return self._create_tree_iter(self.on_get_iter(path))
+
+    @handle_exception(False)
+    def do_iter_next(self, iter):
+        """Internal method."""
+        if iter is None:
+            next_data = self.on_iter_next(None)
+        else:
+            next_data = self.on_iter_next(self.get_user_data(iter))
+
+        self.set_user_data(iter, next_data)
+        return next_data is not None
+
+    @handle_exception(None)
+    def do_get_path(self, iter):
+        """Internal method."""
+        path = self.on_get_path(self.get_user_data(iter))
+        if path is None:
+            return None
+        else:
+            return Gtk.TreePath(path)
+
+    @handle_exception(None)
+    def do_get_value(self, iter, column):
+        """Internal method."""
+        return self.on_get_value(self.get_user_data(iter), column)
+
+    @handle_exception((False, None))
+    def do_iter_children(self, parent):
+        """Internal method."""
+        data = self.get_user_data(parent) if parent else None
+        return self._create_tree_iter(self.on_iter_children(data))
+
+    @handle_exception(False)
+    def do_iter_has_child(self, parent):
+        """Internal method."""
+        return self.on_iter_has_child(self.get_user_data(parent))
+
+    @handle_exception(0)
+    def do_iter_n_children(self, iter):
+        """Internal method."""
+        if iter is None:
+            return self.on_iter_n_children(None)
+        return self.on_iter_n_children(self.get_user_data(iter))
+
+    @handle_exception((False, None))
+    def do_iter_nth_child(self, parent, n):
+        """Internal method."""
+        if parent is None:
+            data = self.on_iter_nth_child(None, n)
+        else:
+            data = self.on_iter_nth_child(self.get_user_data(parent), n)
+        return self._create_tree_iter(data)
+
+    @handle_exception((False, None))
+    def do_iter_parent(self, child):
+        """Internal method."""
+        return self._create_tree_iter(self.on_iter_parent(self.get_user_data(child)))
+
+    @handle_exception(None)
+    def do_ref_node(self, iter):
+        self.on_ref_node(self.get_user_data(iter))
+
+    @handle_exception(None)
+    def do_unref_node(self, iter):
+        self.on_unref_node(self.get_user_data(iter))
+
+    #
+    # Python Subclass Overridables
+    #
+    def on_get_flags(self):
+        """Overridable.
+
+        :Returns Gtk.TreeModelFlags:
+            The flags for this model. See: Gtk.TreeModelFlags
+        """
+        raise NotImplementedError
+
+    def on_get_n_columns(self):
+        """Overridable.
+
+        :Returns:
+            The number of columns for this model.
+        """
+        raise NotImplementedError
+
+    def on_get_column_type(self, index):
+        """Overridable.
+
+        :Returns:
+            The column type for the given index.
+        """
+        raise NotImplementedError
+
+    def on_get_iter(self, path):
+        """Overridable.
+
+        :Returns:
+            A python object (node) for the given TreePath.
+        """
+        raise NotImplementedError
+
+    def on_iter_next(self, node):
+        """Overridable.
+
+        :Parameters:
+            node : object
+                Node at current level.
+
+        :Returns:
+            A python object (node) following the given node at the current level.
+        """
+        raise NotImplementedError
+
+    def on_get_path(self, node):
+        """Overridable.
+
+        :Returns:
+            A TreePath for the given node.
+        """
+        raise NotImplementedError
+
+    def on_get_value(self, node, column):
+        """Overridable.
+
+        :Parameters:
+            node : object
+            column : int
+                Column index to get the value from.
+
+        :Returns:
+            The value of the column for the given node."""
+        raise NotImplementedError
+
+    def on_iter_children(self, parent):
+        """Overridable.
+
+        :Returns:
+            The first child of parent or None if parent has no children.
+            If parent is None, return the first node of the model.
+        """
+        raise NotImplementedError
+
+    def on_iter_has_child(self, node):
+        """Overridable.
+
+        :Returns:
+            True if the given node has children.
+        """
+        raise NotImplementedError
+
+    def on_iter_n_children(self, node):
+        """Overridable.
+
+        :Returns:
+            The number of children for the given node. If node is None,
+            return the number of top level nodes.
+        """
+        raise NotImplementedError
+
+    def on_iter_nth_child(self, parent, n):
+        """Overridable.
+
+        :Parameters:
+            parent : object
+            n : int
+                Index of child within parent.
+
+        :Returns:
+            The child for the given parent index starting at 0. If parent None,
+            return the top level node corresponding to "n".
+            If "n" is larger then available nodes, return None.
+        """
+        raise NotImplementedError
+
+    def on_iter_parent(self, child):
+        """Overridable.
+
+        :Returns:
+            The parent node of child or None if child is a top level node."""
+        raise NotImplementedError
+
+    def on_ref_node(self, node):
+        pass
+
+    def on_unref_node(self, node):
+        pass
diff --git a/pygtkcompat/pygtkcompat.py b/pygtkcompat/pygtkcompat.py
index 18ebd7b..02394f9 100644
--- a/pygtkcompat/pygtkcompat.py
+++ b/pygtkcompat/pygtkcompat.py
@@ -427,6 +427,9 @@ def enable_gtk(version='2.0'):
             value = getattr(Gdk, name)
             setattr(keysyms, target, value)
 
+    from . import generictreemodel
+    Gtk.GenericTreeModel = generictreemodel.GenericTreeModel
+
 
 def enable_vte():
     gi.require_version('Vte', '0.0')
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 1d40539..7ccde54 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -107,6 +107,7 @@ EXTRA_DIST = \
        test_overrides_gdk.py \
        test_overrides_gtk.py \
        test_atoms.py \
+       test_generictreemodel.py \
        compat_test_pygtk.py \
        gi/__init__.py \
        gi/overrides/__init__.py \
diff --git a/tests/test_generictreemodel.py b/tests/test_generictreemodel.py
new file mode 100644
index 0000000..ff0f523
--- /dev/null
+++ b/tests/test_generictreemodel.py
@@ -0,0 +1,406 @@
+# -*- Mode: Python; py-indent-offset: 4 -*-
+# test_generictreemodel - Tests for GenericTreeModel
+# Copyright (C) 2013 Simon Feltman
+#
+#   test_generictreemodel.py: Tests for GenericTreeModel
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+
+# system
+import gc
+import sys
+import weakref
+import unittest
+
+# pygobject
+from gi.repository import GObject
+from gi.repository import Gtk
+from pygtkcompat.generictreemodel import GenericTreeModel
+from pygtkcompat.generictreemodel import _get_user_data_as_pyobject
+
+
+class Node(object):
+    """Represents a generic node with name, value, and children."""
+    def __init__(self, name, value, *children):
+        self.name = name
+        self.value = value
+        self.children = list(children)
+        self.parent = None
+        self.next = None
+
+        for i, child in enumerate(children):
+            child.parent = weakref.ref(self)
+            if i < len(children) - 1:
+                child.next = weakref.ref(children[i + 1])
+
+    def __repr__(self):
+        return 'Node("%s", %s)' % (self.name, self.value)
+
+
+class TesterModel(GenericTreeModel):
+    def __init__(self):
+        super(TesterModel, self).__init__()
+        self.root = Node('root', 0,
+                         Node('spam', 1,
+                              Node('sushi', 2),
+                              Node('bread', 3)
+                         ),
+                         Node('eggs', 4)
+                        )
+
+    def on_get_flags(self):
+        return 0
+
+    def on_get_n_columns(self):
+        return 2
+
+    def on_get_column_type(self, n):
+        return (str, int)[n]
+
+    def on_get_iter(self, path):
+        node = self.root
+        path = list(path)
+        idx = path.pop(0)
+        while path:
+            idx = path.pop(0)
+            node = node.children[idx]
+        return node
+
+    def on_get_path(self, node):
+        def rec_get_path(n):
+            for i, child in enumerate(n.children):
+                if child == node:
+                    return [i]
+                else:
+                    res = rec_get_path(child)
+                    if res:
+                        res.insert(0, i)
+
+        return rec_get_path(self.root)
+
+    def on_get_value(self, node, column):
+        if column == 0:
+            return node.name
+        elif column == 1:
+            return node.value
+
+    def on_iter_has_child(self, node):
+        return bool(node.children)
+
+    def on_iter_next(self, node):
+        if node.next:
+            return node.next()
+
+    def on_iter_children(self, node):
+        if node:
+            return node.children[0]
+        else:
+            return self.root
+
+    def on_iter_n_children(self, node):
+        if node is None:
+            return 1
+        return len(node.children)
+
+    def on_iter_nth_child(self, node, n):
+        if node is None:
+            assert n == 0
+            return self.root
+        return node.children[n]
+
+    def on_iter_parent(self, child):
+        if child.parent:
+            return child.parent()
+
+
+class TestReferences(unittest.TestCase):
+    def setUp(self):
+        pass
+
+    def test_c_tree_iter_user_data_as_pyobject(self):
+        obj = object()
+        obj_id = id(obj)
+        ref_count = sys.getrefcount(obj)
+
+        # This is essentially a stolen ref in the context of _CTreeIter.get_user_data_as_pyobject
+        it = Gtk.TreeIter()
+        it.user_data = obj_id
+
+        obj2 = _get_user_data_as_pyobject(it)
+        self.assertEqual(obj, obj2)
+        self.assertEqual(sys.getrefcount(obj), ref_count + 1)
+
+    def test_leak_references_on(self):
+        model = TesterModel()
+        obj_ref = weakref.ref(model.root)
+        # Initial refcount is 1 for model.root + the temporary
+        self.assertEqual(sys.getrefcount(model.root), 2)
+
+        # Iter increases by 1 do to assignment to iter.user_data
+        res, it = model.do_get_iter([0])
+        self.assertEqual(id(model.root), it.user_data)
+        self.assertEqual(sys.getrefcount(model.root), 3)
+
+        # Verify getting a TreeIter more then once does not further increase
+        # the ref count.
+        res2, it2 = model.do_get_iter([0])
+        self.assertEqual(id(model.root), it2.user_data)
+        self.assertEqual(sys.getrefcount(model.root), 3)
+
+        # Deleting the iter does not decrease refcount because references
+        # leak by default (they are stored in the held_refs pool)
+        del it
+        gc.collect()
+        self.assertEqual(sys.getrefcount(model.root), 3)
+
+        # Deleting a model should free all held references to user data
+        # stored by TreeIters
+        del model
+        gc.collect()
+        self.assertEqual(obj_ref(), None)
+
+    def test_row_deleted_frees_refs(self):
+        model = TesterModel()
+        obj_ref = weakref.ref(model.root)
+        # Initial refcount is 1 for model.root + the temporary
+        self.assertEqual(sys.getrefcount(model.root), 2)
+
+        # Iter increases by 1 do to assignment to iter.user_data
+        res, it = model.do_get_iter([0])
+        self.assertEqual(id(model.root), it.user_data)
+        self.assertEqual(sys.getrefcount(model.root), 3)
+
+        # Notifying the underlying model of a row_deleted should decrease the
+        # ref count.
+        model.row_deleted(Gtk.TreePath('0'), model.root)
+        self.assertEqual(sys.getrefcount(model.root), 2)
+
+        # Finally deleting the actual object should collect it completely
+        del model.root
+        gc.collect()
+        self.assertEqual(obj_ref(), None)
+
+    def test_leak_references_off(self):
+        model = TesterModel()
+        model.leak_references = False
+
+        obj_ref = weakref.ref(model.root)
+        # Initial refcount is 1 for model.root + the temporary
+        self.assertEqual(sys.getrefcount(model.root), 2)
+
+        # Iter does not increas count by 1 when leak_references is false
+        res, it = model.do_get_iter([0])
+        self.assertEqual(id(model.root), it.user_data)
+        self.assertEqual(sys.getrefcount(model.root), 2)
+
+        # Deleting the iter does not decrease refcount because assigning user_data
+        # eats references and does not release them.
+        del it
+        gc.collect()
+        self.assertEqual(sys.getrefcount(model.root), 2)
+
+        # Deleting the model decreases the final ref, and the object is collected
+        del model
+        gc.collect()
+        self.assertEqual(obj_ref(), None)
+
+    def test_iteration_refs(self):
+        # Pull iterators off the model using the wrapped C API which will
+        # then call back into the python overrides.
+        model = TesterModel()
+        nodes = [node for node in model.iter_depth_first()]
+        values = [node.value for node in nodes]
+
+        # Verify depth first ordering
+        self.assertEqual(values, [0, 1, 2, 3, 4])
+
+        # Verify ref counts for each of the nodes.
+        # 5 refs for each node at this point:
+        #   1 - ref held in getrefcount function
+        #   2 - ref held by "node" var during iteration
+        #   3 - ref held by local "nodes" var
+        #   4 - ref held by the root/children graph itself
+        #   5 - ref held by the model "held_refs" instance var
+        for node in nodes:
+            self.assertEqual(sys.getrefcount(node), 5)
+
+        # A second iteration and storage of the nodes in a new list
+        # should only increase refcounts by 1 even though new
+        # iterators are created and assigned.
+        nodes2 = [node for node in model.iter_depth_first()]
+        for node in nodes2:
+            self.assertEqual(sys.getrefcount(node), 6)
+
+        # Hold weak refs and start verifying ref collection.
+        node_refs = [weakref.ref(node) for node in nodes]
+
+        # First round of collection
+        del nodes2
+        gc.collect()
+        for node in nodes:
+            self.assertEqual(sys.getrefcount(node), 5)
+
+        # Second round of collection, no more local lists of nodes.
+        del nodes
+        gc.collect()
+        for ref in node_refs:
+            node = ref()
+            self.assertEqual(sys.getrefcount(node), 4)
+
+        # Using invalidate_iters or row_deleted(path, node) will clear out
+        # the pooled refs held internal to the GenericTreeModel implementation.
+        model.invalidate_iters()
+        self.assertEqual(len(model._held_refs), 0)
+        gc.collect()
+        for ref in node_refs:
+            node = ref()
+            self.assertEqual(sys.getrefcount(node), 3)
+
+        # Deleting the root node at this point should allow all nodes to be collected
+        # as there is no longer a way to reach the children
+        del node  # node still in locals() from last iteration
+        del model.root
+        gc.collect()
+        for ref in node_refs:
+            self.assertEqual(ref(), None)
+
+
+class TestIteration(unittest.TestCase):
+    def test_iter_next_root(self):
+        model = TesterModel()
+        it = model.get_iter([0])
+        self.assertEqual(it.user_data, id(model.root))
+        self.assertEqual(model.root.next, None)
+
+        it = model.iter_next(it)
+        self.assertEqual(it, None)
+
+    def test_iter_next_multiple(self):
+        model = TesterModel()
+        it = model.get_iter([0, 0])
+        self.assertEqual(it.user_data, id(model.root.children[0]))
+
+        it = model.iter_next(it)
+        self.assertEqual(it.user_data, id(model.root.children[1]))
+
+        it = model.iter_next(it)
+        self.assertEqual(it, None)
+
+
+class ErrorModel(GenericTreeModel):
+    # All on_* methods will raise a NotImplementedError by default
+    pass
+
+
+class ExceptHook(object):
+    """
+    Temporarily installs an exception hook in a context which
+    expects the given exc_type to be raised. This allows verification
+    of exceptions that occur within python gi callbacks but
+    are never bubbled through from python to C back to python.
+    This works because exception hooks are called in PyErr_Print.
+    """
+    def __init__(self, exc_type):
+        self._exc_type = exc_type
+        self._exceptions = []
+
+    def _excepthook(self, exc_type, value, traceback):
+        self._exceptions.append(exc_type)
+
+    def __enter__(self):
+        self._oldhook = sys.excepthook
+        sys.excepthook = self._excepthook
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        sys.excepthook = self._oldhook
+        assert len(self._exceptions) == 1, 'Expecting exactly one exception of type %s' % self._exc_type
+        assert issubclass(self._exceptions[0], self._exc_type), 'Expecting exactly one exception of type %s' 
% self._exc_type
+
+
+class TestReturnsAfterError(unittest.TestCase):
+    def setUp(self):
+        self.model = ErrorModel()
+
+    def test_get_flags(self):
+        with ExceptHook(NotImplementedError):
+            flags = self.model.get_flags()
+        self.assertEqual(flags, 0)
+
+    def test_get_n_columns(self):
+        with ExceptHook(NotImplementedError):
+            count = self.model.get_n_columns()
+        self.assertEqual(count, 0)
+
+    def test_get_column_type(self):
+        with ExceptHook(NotImplementedError):
+            col_type = self.model.get_column_type(0)
+        self.assertEqual(col_type, GObject.TYPE_INVALID)
+
+    def test_get_iter(self):
+        with ExceptHook(NotImplementedError):
+            self.assertRaises(ValueError, self.model.get_iter, Gtk.TreePath(0))
+
+    def test_get_path(self):
+        it = self.model.create_tree_iter('foo')
+        with ExceptHook(NotImplementedError):
+            path = self.model.get_path(it)
+        self.assertEqual(path, None)
+
+    def test_get_value(self):
+        it = self.model.create_tree_iter('foo')
+        with ExceptHook(NotImplementedError):
+            try:
+                self.model.get_value(it, 0)
+            except TypeError:
+                pass  # silence TypeError converting None to GValue
+
+    def test_iter_has_child(self):
+        it = self.model.create_tree_iter('foo')
+        with ExceptHook(NotImplementedError):
+            res = self.model.iter_has_child(it)
+        self.assertEqual(res, False)
+
+    def test_iter_next(self):
+        it = self.model.create_tree_iter('foo')
+        with ExceptHook(NotImplementedError):
+            res = self.model.iter_next(it)
+        self.assertEqual(res, None)
+
+    def test_iter_children(self):
+        with ExceptHook(NotImplementedError):
+            res = self.model.iter_children(None)
+        self.assertEqual(res, None)
+
+    def test_iter_n_children(self):
+        with ExceptHook(NotImplementedError):
+            res = self.model.iter_n_children(None)
+        self.assertEqual(res, 0)
+
+    def test_iter_nth_child(self):
+        with ExceptHook(NotImplementedError):
+            res = self.model.iter_nth_child(None, 0)
+        self.assertEqual(res, None)
+
+    def test_iter_parent(self):
+        child = self.model.create_tree_iter('foo')
+        with ExceptHook(NotImplementedError):
+            res = self.model.iter_parent(child)
+        self.assertEqual(res, None)
+
+if __name__ == '__main__':
+    unittest.main()


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]