functions: revert the function init order to make pylint happy again. See #217
[pygobject.git] / tests / test_generictreemodel.py
blob5e9d716e99106c6828a808552b1d6d407ea80e3b
1 # -*- Mode: Python; py-indent-offset: 4 -*-
2 # test_generictreemodel - Tests for GenericTreeModel
3 # Copyright (C) 2013 Simon Feltman
5 # test_generictreemodel.py: Tests for GenericTreeModel
7 # This library is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU Lesser General Public
9 # License as published by the Free Software Foundation; either
10 # version 2.1 of the License, or (at your option) any later version.
12 # This library is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # Lesser General Public License for more details.
17 # You should have received a copy of the GNU Lesser General Public
18 # License along with this library; if not, see <http://www.gnu.org/licenses/>.
20 from __future__ import absolute_import
22 # system
23 import gc
24 import sys
25 import weakref
26 import unittest
27 import platform
29 # pygobject
30 from gi.repository import GObject
32 try:
33 from gi.repository import Gtk
34 from pygtkcompat.generictreemodel import GenericTreeModel
35 from pygtkcompat.generictreemodel import _get_user_data_as_pyobject
36 has_gtk = True
37 except ImportError:
38 GenericTreeModel = object
39 has_gtk = False
42 class Node(object):
43 """Represents a generic node with name, value, and children."""
44 def __init__(self, name, value, *children):
45 self.name = name
46 self.value = value
47 self.children = list(children)
48 self.parent = None
49 self.next = None
51 for i, child in enumerate(children):
52 child.parent = weakref.ref(self)
53 if i < len(children) - 1:
54 child.next = weakref.ref(children[i + 1])
56 def __repr__(self):
57 return 'Node("%s", %s)' % (self.name, self.value)
60 class ATesterModel(GenericTreeModel):
61 def __init__(self):
62 super(ATesterModel, self).__init__()
63 self.root = Node('root', 0,
64 Node('spam', 1,
65 Node('sushi', 2),
66 Node('bread', 3)
68 Node('eggs', 4)
71 def on_get_flags(self):
72 return 0
74 def on_get_n_columns(self):
75 return 2
77 def on_get_column_type(self, n):
78 return (str, int)[n]
80 def on_get_iter(self, path):
81 node = self.root
82 path = list(path)
83 idx = path.pop(0)
84 while path:
85 idx = path.pop(0)
86 node = node.children[idx]
87 return node
89 def on_get_path(self, node):
90 def rec_get_path(n):
91 for i, child in enumerate(n.children):
92 if child == node:
93 return [i]
94 else:
95 res = rec_get_path(child)
96 if res:
97 res.insert(0, i)
99 return rec_get_path(self.root)
101 def on_get_value(self, node, column):
102 if column == 0:
103 return node.name
104 elif column == 1:
105 return node.value
107 def on_iter_has_child(self, node):
108 return bool(node.children)
110 def on_iter_next(self, node):
111 if node.next:
112 return node.next()
114 def on_iter_children(self, node):
115 if node:
116 return node.children[0]
117 else:
118 return self.root
120 def on_iter_n_children(self, node):
121 if node is None:
122 return 1
123 return len(node.children)
125 def on_iter_nth_child(self, node, n):
126 if node is None:
127 assert n == 0
128 return self.root
129 return node.children[n]
131 def on_iter_parent(self, child):
132 if child.parent:
133 return child.parent()
136 @unittest.skipUnless(has_gtk, 'Gtk not available')
137 class TestReferences(unittest.TestCase):
138 def setUp(self):
139 pass
141 @unittest.skipIf(platform.python_implementation() == "PyPy", "not with PyPy")
142 def test_c_tree_iter_user_data_as_pyobject(self):
143 obj = object()
144 obj_id = id(obj)
145 ref_count = sys.getrefcount(obj)
147 # This is essentially a stolen ref in the context of _CTreeIter.get_user_data_as_pyobject
148 it = Gtk.TreeIter()
149 it.user_data = obj_id
151 obj2 = _get_user_data_as_pyobject(it)
152 self.assertEqual(obj, obj2)
153 self.assertEqual(sys.getrefcount(obj), ref_count + 1)
155 def test_leak_references_on(self):
156 model = ATesterModel()
157 obj_ref = weakref.ref(model.root)
158 # Initial refcount is 1 for model.root + the temporary
159 if hasattr(sys, "getrefcount"):
160 self.assertEqual(sys.getrefcount(model.root), 2)
162 # Iter increases by 1 do to assignment to iter.user_data
163 res, it = model.do_get_iter([0])
164 self.assertEqual(id(model.root), it.user_data)
165 if hasattr(sys, "getrefcount"):
166 self.assertEqual(sys.getrefcount(model.root), 3)
168 # Verify getting a TreeIter more then once does not further increase
169 # the ref count.
170 res2, it2 = model.do_get_iter([0])
171 self.assertEqual(id(model.root), it2.user_data)
172 if hasattr(sys, "getrefcount"):
173 self.assertEqual(sys.getrefcount(model.root), 3)
175 # Deleting the iter does not decrease refcount because references
176 # leak by default (they are stored in the held_refs pool)
177 del it
178 gc.collect()
179 if hasattr(sys, "getrefcount"):
180 self.assertEqual(sys.getrefcount(model.root), 3)
182 # Deleting a model should free all held references to user data
183 # stored by TreeIters
184 del model
185 gc.collect()
186 self.assertEqual(obj_ref(), None)
188 def test_row_deleted_frees_refs(self):
189 model = ATesterModel()
190 obj_ref = weakref.ref(model.root)
191 # Initial refcount is 1 for model.root + the temporary
192 if hasattr(sys, "getrefcount"):
193 self.assertEqual(sys.getrefcount(model.root), 2)
195 # Iter increases by 1 do to assignment to iter.user_data
196 res, it = model.do_get_iter([0])
197 self.assertEqual(id(model.root), it.user_data)
198 if hasattr(sys, "getrefcount"):
199 self.assertEqual(sys.getrefcount(model.root), 3)
201 # Notifying the underlying model of a row_deleted should decrease the
202 # ref count.
203 model.row_deleted(Gtk.TreePath('0'), model.root)
204 if hasattr(sys, "getrefcount"):
205 self.assertEqual(sys.getrefcount(model.root), 2)
207 # Finally deleting the actual object should collect it completely
208 del model.root
209 gc.collect()
210 self.assertEqual(obj_ref(), None)
212 def test_leak_references_off(self):
213 model = ATesterModel()
214 model.leak_references = False
216 obj_ref = weakref.ref(model.root)
217 # Initial refcount is 1 for model.root + the temporary
218 if hasattr(sys, "getrefcount"):
219 self.assertEqual(sys.getrefcount(model.root), 2)
221 # Iter does not increas count by 1 when leak_references is false
222 res, it = model.do_get_iter([0])
223 self.assertEqual(id(model.root), it.user_data)
224 if hasattr(sys, "getrefcount"):
225 self.assertEqual(sys.getrefcount(model.root), 2)
227 # Deleting the iter does not decrease refcount because assigning user_data
228 # eats references and does not release them.
229 del it
230 gc.collect()
231 if hasattr(sys, "getrefcount"):
232 self.assertEqual(sys.getrefcount(model.root), 2)
234 # Deleting the model decreases the final ref, and the object is collected
235 del model
236 gc.collect()
237 self.assertEqual(obj_ref(), None)
239 def test_iteration_refs(self):
240 # Pull iterators off the model using the wrapped C API which will
241 # then call back into the python overrides.
242 model = ATesterModel()
243 nodes = [node for node in model.iter_depth_first()]
244 values = [node.value for node in nodes]
246 # Verify depth first ordering
247 self.assertEqual(values, [0, 1, 2, 3, 4])
249 # Verify ref counts for each of the nodes.
250 # 5 refs for each node at this point:
251 # 1 - ref held in getrefcount function
252 # 2 - ref held by "node" var during iteration
253 # 3 - ref held by local "nodes" var
254 # 4 - ref held by the root/children graph itself
255 # 5 - ref held by the model "held_refs" instance var
256 for node in nodes:
257 if hasattr(sys, "getrefcount"):
258 self.assertEqual(sys.getrefcount(node), 5)
260 # A second iteration and storage of the nodes in a new list
261 # should only increase refcounts by 1 even though new
262 # iterators are created and assigned.
263 nodes2 = [node for node in model.iter_depth_first()]
264 for node in nodes2:
265 if hasattr(sys, "getrefcount"):
266 self.assertEqual(sys.getrefcount(node), 6)
268 # Hold weak refs and start verifying ref collection.
269 node_refs = [weakref.ref(node) for node in nodes]
271 # First round of collection
272 del nodes2
273 gc.collect()
274 for node in nodes:
275 if hasattr(sys, "getrefcount"):
276 self.assertEqual(sys.getrefcount(node), 5)
278 # Second round of collection, no more local lists of nodes.
279 del nodes
280 gc.collect()
281 for ref in node_refs:
282 node = ref()
283 if hasattr(sys, "getrefcount"):
284 self.assertEqual(sys.getrefcount(node), 4)
286 # Using invalidate_iters or row_deleted(path, node) will clear out
287 # the pooled refs held internal to the GenericTreeModel implementation.
288 model.invalidate_iters()
289 self.assertEqual(len(model._held_refs), 0)
290 gc.collect()
291 for ref in node_refs:
292 node = ref()
293 if hasattr(sys, "getrefcount"):
294 self.assertEqual(sys.getrefcount(node), 3)
296 # Deleting the root node at this point should allow all nodes to be collected
297 # as there is no longer a way to reach the children
298 del node # node still in locals() from last iteration
299 del model.root
300 gc.collect()
301 for ref in node_refs:
302 self.assertEqual(ref(), None)
305 @unittest.skipUnless(has_gtk, 'Gtk not available')
306 class TestIteration(unittest.TestCase):
307 def test_iter_next_root(self):
308 model = ATesterModel()
309 it = model.get_iter([0])
310 self.assertEqual(it.user_data, id(model.root))
311 self.assertEqual(model.root.next, None)
313 it = model.iter_next(it)
314 self.assertEqual(it, None)
316 def test_iter_next_multiple(self):
317 model = ATesterModel()
318 it = model.get_iter([0, 0])
319 self.assertEqual(it.user_data, id(model.root.children[0]))
321 it = model.iter_next(it)
322 self.assertEqual(it.user_data, id(model.root.children[1]))
324 it = model.iter_next(it)
325 self.assertEqual(it, None)
328 class ErrorModel(GenericTreeModel):
329 # All on_* methods will raise a NotImplementedError by default
330 pass
333 @unittest.skipUnless(has_gtk, 'Gtk not available')
334 class ExceptHook(object):
336 Temporarily installs an exception hook in a context which
337 expects the given exc_type to be raised. This allows verification
338 of exceptions that occur within python gi callbacks but
339 are never bubbled through from python to C back to python.
340 This works because exception hooks are called in PyErr_Print.
342 def __init__(self, *expected_exc_types):
343 self._expected_exc_types = expected_exc_types
344 self._exceptions = []
346 def _excepthook(self, exc_type, value, traceback):
347 self._exceptions.append((exc_type, value))
349 def __enter__(self):
350 self._oldhook = sys.excepthook
351 sys.excepthook = self._excepthook
352 return self
354 def __exit__(self, exc_type, exc_val, exc_tb):
355 sys.excepthook = self._oldhook
356 error_message = 'Expecting the following exceptions: %s, got: %s' % \
357 (str(self._expected_exc_types), '\n'.join([str(item) for item in self._exceptions]))
359 assert len(self._expected_exc_types) == len(self._exceptions), error_message
361 for expected, got in zip(self._expected_exc_types, [exc[0] for exc in self._exceptions]):
362 assert issubclass(got, expected), error_message
365 @unittest.skipUnless(has_gtk, 'Gtk not available')
366 class TestReturnsAfterError(unittest.TestCase):
367 def setUp(self):
368 self.model = ErrorModel()
370 def test_get_flags(self):
371 with ExceptHook(NotImplementedError):
372 flags = self.model.get_flags()
373 self.assertEqual(flags, 0)
375 def test_get_n_columns(self):
376 with ExceptHook(NotImplementedError):
377 count = self.model.get_n_columns()
378 self.assertEqual(count, 0)
380 def test_get_column_type(self):
381 with ExceptHook(NotImplementedError, TypeError):
382 col_type = self.model.get_column_type(0)
383 self.assertEqual(col_type, GObject.TYPE_INVALID)
385 def test_get_iter(self):
386 with ExceptHook(NotImplementedError):
387 self.assertRaises(ValueError, self.model.get_iter, Gtk.TreePath(0))
389 def test_get_path(self):
390 it = self.model.create_tree_iter('foo')
391 with ExceptHook(NotImplementedError):
392 path = self.model.get_path(it)
393 self.assertEqual(path, None)
395 def test_get_value(self):
396 it = self.model.create_tree_iter('foo')
397 with ExceptHook(NotImplementedError):
398 try:
399 self.model.get_value(it, 0)
400 except TypeError:
401 pass # silence TypeError converting None to GValue
403 def test_iter_has_child(self):
404 it = self.model.create_tree_iter('foo')
405 with ExceptHook(NotImplementedError):
406 res = self.model.iter_has_child(it)
407 self.assertEqual(res, False)
409 def test_iter_next(self):
410 it = self.model.create_tree_iter('foo')
411 with ExceptHook(NotImplementedError):
412 res = self.model.iter_next(it)
413 self.assertEqual(res, None)
415 def test_iter_children(self):
416 with ExceptHook(NotImplementedError):
417 res = self.model.iter_children(None)
418 self.assertEqual(res, None)
420 def test_iter_n_children(self):
421 with ExceptHook(NotImplementedError):
422 res = self.model.iter_n_children(None)
423 self.assertEqual(res, 0)
425 def test_iter_nth_child(self):
426 with ExceptHook(NotImplementedError):
427 res = self.model.iter_nth_child(None, 0)
428 self.assertEqual(res, None)
430 def test_iter_parent(self):
431 child = self.model.create_tree_iter('foo')
432 with ExceptHook(NotImplementedError):
433 res = self.model.iter_parent(child)
434 self.assertEqual(res, None)