pax_global_header00006660000000000000000000000064142042530520014507gustar00rootroot0000000000000052 comment=628f5887da9b3d3fe4dcf9d021d85539d0ede279 liblarch-3.2.0/000077500000000000000000000000001420425305200132715ustar00rootroot00000000000000liblarch-3.2.0/.gitignore000066400000000000000000000000431420425305200152560ustar00rootroot00000000000000*.pyc .*.sw* *.prof MANIFEST dist/ liblarch-3.2.0/.travis.yml000066400000000000000000000005021420425305200153770ustar00rootroot00000000000000language: python python: - '3.2' before_install: - sudo apt-get update - sudo apt-get install -qq python3-gi gir1.2-gtk-3.0 before_script: - "export DISPLAY=:99.0" - "sh -e /etc/init.d/xvfb start" virtualenv: system_site_packages: true install: - pip install -r requirements.txt script: - make check liblarch-3.2.0/AUTHORS000066400000000000000000000003401420425305200143360ustar00rootroot00000000000000AUTHORS ======== Liblarch team: -------------- * Lionel Dricot * Izidor Matušov Contributors: ------------- * Antonio Roquentin liblarch-3.2.0/CHANGELOG000066400000000000000000000027461420425305200145140ustar00rootroot000000000000002022-02-19 liblarch 3.2.0 * Provide a fix for collapsing/expanding future (not-yet-created) nodes This prevents a TypeError under various circumstances, which made GTG's TreeView display inconsistent search/filtering results. * Add PyPI metadata to setup.py * Clarify, in the README file, that liblarch is LGPL v3 "or later" * Fix Pytest deprecation warnings 2021-03-31 liblarch 3.1.0 * Provide an optimized way to refresh filtered items This provides better performance for GTG in particular * Replace calls to the pep8 executable by pycodestyle * Disable building with Python 2 2020-06-04 liblarch 3.0.1 * Fix drag & drop from one GTK TreeView widget to another * Handle cases where the tree should and shouldn't be re-filtered * Improved PyGI (GObject introspection) compatibility * Improved code quality and PEP 8 compliance 2014-04-19 liblarch 3.0.0 * Port to Python 3, GObject introspection and GTK 3 2013-01-22 liblarch 2.2.0 * Fix an incorrect node count in a callback (LP #1078368) 2012-11-08 liblarch 2.1.0 * Introducing the changelog * Removed completely the "transparency" property * Added a new object : viewcount * The version number of liblarch will now be the API number + a number. Meaning that: - 0.0.1 releases are pure bugfix/performance releases without impact on your application - 0.1.0 releases introduce new API but are backward compatible. You don't need to port your application. - 1.0.0 releases break the API. liblarch-3.2.0/LICENSE000066400000000000000000000167431420425305200143110ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. liblarch-3.2.0/MANIFEST.in000066400000000000000000000000571420425305200150310ustar00rootroot00000000000000include README include LICENSE include AUTHORS liblarch-3.2.0/Makefile000066400000000000000000000032061420425305200147320ustar00rootroot00000000000000# ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- # Simple makefile for common tasks check: tests lint tests: ./run-tests sdist: python setup.py sdist # Remove .pyc files clean: find -type d -name '__pycache__' -print | xargs rm -rf find -type f -iname '*.~*~' -exec rm {} \; rm -f *.bak rm -rf dist/ # Check for common & easily catchable Python mistakes. pyflakes: pyflakes examples liblarch liblarch_gtk tests \ main.py run-tests setup.py # Check for coding standard violations. pep8: pycodestyle --statistics --count --repeat --max-line-length=110 --ignore=E128,W504 \ examples liblarch liblarch_gtk tests main.py run-tests setup.py # Check for coding standard violations & flakes. lint: pyflakes pycodestyle .PHONY: check tests sdist clean pyflakes pycodestyle lint liblarch-3.2.0/README.md000066400000000000000000000017011420425305200145470ustar00rootroot00000000000000# Liblarch [![Build Status](https://travis-ci.org/getting-things-gnome/liblarch.svg)](https://travis-ci.org/getting-things-gnome/liblarch) If you find Gtk.Treeview and Gtk.Treemodel hard to use, then liblarch is probably for you. Liblarch is a python library built to easily handle data structure such are lists, trees and acyclic graphs (tree where nodes can have multiple parents). There's also a liblarch-gtk binding that will allow you to use your data structure into a Gtk.Treeview. Liblarch support multiple views of one data structure and complex filtering. That way, you have a clear separation between your data themselves (Model) and how they are displayed (View). ## Links - [Documentation](https://wiki.gnome.org/Projects/liblarch) ## Credits Liblarch is published under the LGPLv3 license, or (at your option) any later version. Authors: - [Lionel Dricot](https://github.com/ploum) - [Izidor Matušov](https://github.com/izidormatusov) liblarch-3.2.0/debian/000077500000000000000000000000001420425305200145135ustar00rootroot00000000000000liblarch-3.2.0/debian/changelog000066400000000000000000000052251420425305200163710ustar00rootroot00000000000000liblarch (3.2.0-1) unstable; urgency=medium * Provide a fix for collapsing/expanding future (not-yet-created) nodes This prevents a TypeError under various circumstances, which made GTG's TreeView display inconsistent search/filtering results. * Add PyPI metadata to setup.py * Clarify, in the README file, that liblarch is LGPL v3 "or later" * Fix Pytest deprecation warnings -- Jeff F. Sat, 19 Feb 2022 13:37:00 -0400 liblarch (3.1.0-1) unstable; urgency=medium * Provide an optimized way to refresh filtered items This provides better performance for GTG in particular * Replace calls to the pep8 executable by pycodestyle * Disable building with Python 2 -- Jeff F. Wed, 31 Mar 2021 13:37:00 -0400 liblarch (3.0.1-1) unstable; urgency=low * Release accumulated improvements: - Fix drag & drop from one GTK TreeView widget to another - Handle cases where the tree should and shouldn't be re-filtered - Improved PyGI (GObject introspection) compatibility - Improved code quality and PEP 8 compliance -- Jeff F. Fri, 4 Jun 2020 13:37:00 -0400 liblarch (v3.0-26-g2b3366b-1) unstable; urgency=medium * New upstream snapshot * Debian packaging changes: - Switch dh build system to pybuild - Run test suite when building, using Xvfb - Bump debhelper compatibility level to 10 - Reworded package synopsis line - Fixed package description indentation - Fixed typo in package description - Removed unnecessary X-Python3-Version header - Bumped Standards-Version to 4.5.0 -- Frédéric Brière Wed, 06 May 2020 20:01:04 -0400 liblarch (3.0.0-1) unstable; urgency=low * Port to Python 3, GObject introspection and GTK 3 -- Izidor Matušov Sat, 19 Apr 2014 09:44:12 +0100 liblarch (2.2.0-1) unstable; urgency=low * Fix for LP #1078368: incorrect node count in a callback -- Izidor Matušov Tue, 22 Jan 2013 15:56:12 +0100 liblarch (2.1.0-1) unstable; urgency=low * Removed completely the "transparency" property * Added a new object: viewcount * The version number of liblarch will now be the API number + a number. Meaning that: 0.0.1 releases are pure bugfix/performance releases without impact on your application 0.1.0 releases introduce new API but are backward compatible. You don't need to port your application. 1.0.0 releases break the API. -- Lionel Dricot Thu, 08 Nov 2012 12:00:00 +0200 liblarch (0.2.5-1) unstable; urgency=low * Repackage liblarch and liblarch_gtk into a single package -- Izidor Matušov Sat, 11 Aug 2012 16:50:51 +0200 liblarch-3.2.0/debian/compat000066400000000000000000000000031420425305200157120ustar00rootroot0000000000000010 liblarch-3.2.0/debian/control000066400000000000000000000013401420425305200161140ustar00rootroot00000000000000Source: liblarch Priority: optional Maintainer: Izidor Matušov Build-Depends: debhelper (>= 10~), dh-python, python3-all, python3-setuptools, # Needed for running the test suite: python3-nose, python3-gi, gir1.2-gtk-3.0, xvfb, xauth, Standards-Version: 4.5.0 Section: python Homepage: https://github.com/getting-things-gnome/liblarch Package: python3-liblarch Architecture: all Depends: ${misc:Depends}, ${python3:Depends}, python3-gi, gir1.2-gtk-3.0 Description: Python library to easily handle complex data structures Liblarch is a Python library built to easily handle data structures such as lists, trees and directed acyclic graphs and represent them as a GTK TreeWidget or in other forms. liblarch-3.2.0/debian/copyright000066400000000000000000000020371420425305200164500ustar00rootroot00000000000000Format: http://dep.debian.net/deps/dep5 Upstream-Name: liblarch Source: https://github.com/getting-things-gnome/liblarch Files: * Copyright: 2011-2012 Lionel Dricot 2011-2012 Izidor Matušov License: LGPL-3 This program 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 3 of the License, or (at your option) any later version. . This program 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 program. If not, see . . On Debian systems, the complete text of the GNU Lesser General Public License version 3 can be found in "/usr/share/common-licenses/LGPL-3". liblarch-3.2.0/debian/docs000066400000000000000000000000221420425305200153600ustar00rootroot00000000000000AUTHORS README.md liblarch-3.2.0/debian/rules000077500000000000000000000005131420425305200155720ustar00rootroot00000000000000#!/usr/bin/make -f export PYBUILD_NAME = liblarch %: dh $@ --with python3 --buildsystem=pybuild # Use Xvfb to run tests without requiring an actual X server # (We need to check $DEB_BUILD_OPTIONS ourselves until Debhelper 13+) override_dh_auto_test: ifeq (,$(filter nocheck, $(DEB_BUILD_OPTIONS))) xvfb-run dh_auto_test endif liblarch-3.2.0/debian/source/000077500000000000000000000000001420425305200160135ustar00rootroot00000000000000liblarch-3.2.0/debian/source/format000066400000000000000000000000141420425305200172210ustar00rootroot000000000000003.0 (quilt) liblarch-3.2.0/examples/000077500000000000000000000000001420425305200151075ustar00rootroot00000000000000liblarch-3.2.0/examples/artefact000066400000000000000000000002121420425305200166160ustar00rootroot00000000000000Tree before operation ==================== Tree ==================== root C D X X ============================================== liblarch-3.2.0/examples/contact_list/000077500000000000000000000000001420425305200175755ustar00rootroot00000000000000liblarch-3.2.0/examples/contact_list/contact_list.py000077500000000000000000000331161420425305200226440ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2015 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- # The following code is an example that build a GTK contact-list with liblarch # If you have some basic PyGTK experience, the code should be straightforward. from gi.repository import Gtk, GObject import cairo import sys # First, we import this liblarch try: sys.path.append("../../../liblarch") from liblarch import Tree, TreeNode from liblarch_gtk import TreeView except ImportError: raise # The contacts (this is a static list for the purpose of the example) # We have the following: # 1: XMPP address # 2: status: online, busy or offline # 3: Name or nickname # 4: The teams in which the contact is CONTACTS = [ { 'xmpp': 'me@myself.com', 'name': 'Myself', 'status': 'online', 'teams': [], }, { 'xmpp': 'ploum@gtg.net', 'name': 'Lionel', 'status': 'online', 'teams': ['gtg', 'gnome'], }, { 'xmpp': 'izidor@gtg.net', 'name': 'Izidor', 'status': 'busy', 'teams': ['gtg', 'gnome'], }, { 'xmpp': 'bertrand@gtg.net', 'name': 'Bertrand', 'status': 'offline', 'teams': ['gtg', 'on holidays'], }, { 'xmpp': 'joe@dalton.com', 'name': 'Joe Dalton', 'status': 'busy', 'teams': ['daltons'], }, { 'xmpp': 'jack@dalton.com', 'name': 'Jack Dalton', 'status': 'offline', 'teams': ['daltons'], }, { 'xmpp': 'william@dalton.com', 'name': 'William Dalton', 'status': 'offline', 'teams': ['daltons', 'on holidays'], }, { 'xmpp': 'averell@dalton.com', 'name': 'Averell Dalton', 'status': 'online', 'teams': ['daltons'], }, { 'xmpp': 'guillaume@gnome.org', 'name': 'Guillaume (Ploum)', 'status': 'busy', 'teams': ['gnome'], }, { 'xmpp': 'xavier@gnome.org', 'name': 'Navier', 'status': 'busy', 'teams': ['gnome'], }, { 'xmpp': 'vincent@gnome.org', 'name': 'Nice Hat', 'status': 'busy', 'teams': ['gnome'], }, ] class NodeContact(TreeNode): """ This is the "Contact" object. """ def __init__(self, node_id): self.status = "online" self.nick = "" TreeNode.__init__(self, node_id) # A contact cannot have children node. We disable that self.set_children_enabled(False) def get_type(self): return "contact" def set_status(self, status): self.status = status self.modified() def get_status(self): return self.status def set_nick(self, nick): self.nick = nick self.modified() def get_nick(self): return self.nick def get_label(self): """ The label is: - the nickname in bold - XMPP address (small) """ if self.status == "offline": label = "%s" % self.nick else: label = "%s" % self.nick label += " (%s)" % ( self.get_id()) return label class NodeTeam(TreeNode): """ Each team is also a node """ def __init__(self, node_id): TreeNode.__init__(self, node_id) # A team cannot have parents. This is arbitrarly done for the purpose # of this example. self.set_parents_enabled(False) def get_type(self): return "team" def get_label(self): return self.get_id() def get_status(self): return None class ContactListWindow(object): def __init__(self): # First we do all the GTK stuff # This is not interesting from a liblarch perspective self.window = Gtk.Window() self.window.set_size_request(300, 600) self.window.set_border_width(12) self.window.set_title('Liblarch contact-list') self.window.connect('destroy', self.quit) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) vbox.set_spacing(6) # A check button to show/hide offline contacts show_offline = Gtk.CheckButton("Show offline contacts") show_offline.connect("toggled", self.show_offline_contacts) vbox.pack_start(show_offline, expand=False, fill=True, padding=0) # The search through contacts search = Gtk.Entry() search.set_icon_from_icon_name(0, "search") search.get_buffer().connect("inserted-text", self.search) search.get_buffer().connect("deleted-text", self.search) vbox.pack_start(search, expand=False, fill=True, padding=0) # The contact list, build with liblarch scrolled_window = Gtk.ScrolledWindow() scrolled_window.add_with_viewport(self.make_contact_list()) scrolled_window.set_policy( Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) vbox.pack_start(scrolled_window, True, True, 0) # Status box = Gtk.ComboBoxText() box.append_text("Online") box.append_text("Busy") box.append_text("Offline") box.set_active(0) box.connect('changed', self.status_changed) vbox.pack_start(box, expand=False, fill=True, padding=0) self.window.add(vbox) self.window.show_all() # This is the interesting part on how we use liblarch def make_contact_list(self): # LIBLARCH TREE CONSTRUCTION # First thing, we create a liblarch tree self.tree = Tree() # Now, we add each contact *and* each team as nodes of that tree. # The team will be the parents of the contact nodes. for contact in CONTACTS: # We create the node and use the XMPP address as the node_id node = NodeContact(contact['xmpp']) # We add the status and the nickname node.set_status(contact['status']) node.set_nick(contact['name']) # The contact node is added to the tree self.tree.add_node(node) # Now, we create the team if it was not done before for team_name in contact['teams']: if not self.tree.has_node(team_name): team_node = NodeTeam(team_name) self.tree.add_node(team_node) # now we put the contact under the team node.add_parent(team_name) # we could also have done # team_node.add_child(contact[0]) # LIBLARCH VIEW and FILTER # Ok, now we have our liblarch tree. What we need is a view. self.view = self.tree.get_viewtree() # We also create a filter that will allow us to hide offline people self.tree.add_filter("online", self.is_node_online) self.offline = False self.tree.add_filter("search", self.search_filter) # And we apply this filter by default self.view.apply_filter("online") # LIBLARCH GTK.TreeView # And, now, we build our Gtk.TreeView # We will build each column of our TreeView columns = {} # The first column contain the XMPP address but will be hidden # But it is still useful for searching col = {} col['value'] = [str, lambda node: node.get_id()] col['visible'] = False col['order'] = 0 columns['XMPP'] = col # The second column is the status col = {} render_tags = CellRendererTags() render_tags.set_property('xalign', 0.0) col['renderer'] = ['status', render_tags] col['value'] = [GObject.TYPE_PYOBJECT, lambda node: node.get_status()] col['expandable'] = False col['resizable'] = False col['order'] = 1 columns['status'] = col # the third column is the nickname col = {} col['value'] = [str, lambda node: node.get_label()] col['visible'] = True col['order'] = 2 columns['nick'] = col return TreeView(self.view, columns) # This is the "online" filter. # It returns the contacts that are busy or online # and teams that have at least one contact displayed def is_node_online(self, node): if node.get_type() == "contact": # Always show myself if node.get_id() == 'me@myself.com': return True status = node.get_status() if status == "online" or status == "busy": return True else: return False # For the team, we test each contact of that team elif node.get_type() == "team": tree = node.get_tree() for child_id in node.get_children(): child = tree.get_node(child_id) status = child.get_status() if status == "online" or status == "busy": return True return False return True def show_offline_contacts(self, widget): # We should remove the filter to show offline contacts if widget.get_active(): self.view.unapply_filter('online') self.offline = True # else we apply the "online" filter, showing only online/busy people else: self.view.apply_filter('online') self.offline = False def status_changed(self, widget): new = widget.get_active_text() node = self.tree.get_node('me@myself.com') if new == 'Busy': node.set_status('busy') elif new == 'Offline': node.set_status('offline') else: node.set_status('online') def search(self, widget, position, char, nchar=None): search_string = widget.get_text() if len(search_string) > 0: # First, we remove the old filter # Note the "refresh=False", because we know we will apply another # filter just afterwards # We also remove the online filter to search through offline # contacts if not self.offline: self.view.unapply_filter('online', refresh=False) self.view.unapply_filter('search', refresh=False) self.view.apply_filter( 'search', parameters={'search': search_string}) else: if not self.offline: self.view.apply_filter('online') self.view.unapply_filter('search') def search_filter(self, node, parameters=None): string = parameters['search'] if node.get_type() == "contact": if string in node.get_id() or string in node.get_nick(): return True else: return False else: return False def quit(self, widget): Gtk.main_quit() class CellRendererTags(Gtk.CellRenderer): """ Custom CellRenderer that will make a coloured circle This is aboslutely not needed for liblarch. The purpose of using it is to show that liblarch works with complex cellrenderer too. """ __gproperties__ = { 'status': ( GObject.TYPE_PYOBJECT, "Status", "Status", GObject.PARAM_READWRITE, ), } def __init__(self): super(CellRendererTags, self).__init__() self.status = None self.xpad = 1 self.ypad = 1 self.PADDING = 1 def do_set_property(self, pspec, value): if pspec.name == "status": self.status = value else: setattr(self, pspec.name, value) def do_get_property(self, pspec): if pspec.name == "status": return self.status else: return getattr(self, pspec.name) def do_render(self, cr, widget, background_area, cell_area, flags): cr.set_antialias(cairo.ANTIALIAS_NONE) # Coordinates of the origin point y_align = self.get_property("yalign") rect_x = cell_area.x rect_y = cell_area.y + int((cell_area.height - 16) * y_align) colours = { "online": (0.059, 0.867, 0.157), "busy": (0.910, 0.067, 0.063), "offline": (0.467, 0.467, 0.467), } if self.status: color = colours[self.status] # Draw circle radius = 7 cr.set_source_rgb(*color) cr.arc(rect_x, rect_y + 8, radius, 90, 180) cr.fill() # Outer line cr.set_source_rgba(0, 0, 0, 0.20) cr.set_line_width(1.0) cr.arc(rect_x, rect_y + 8, radius, 90, 180) cr.stroke() def do_get_size(self, widget, cell_area=None): if self.status: return ( self.xpad, self.ypad, self.xpad * 2 + 16 + 2 * self.PADDING, self.ypad * 2 + 16, ) else: return (0, 0, 0, 0) GObject.type_register(CellRendererTags) if __name__ == "__main__": # We launch the GTK main_loop ContactListWindow() Gtk.main() liblarch-3.2.0/examples/cycle000066400000000000000000000002321420425305200161260ustar00rootroot00000000000000Tree before operation ==================== Tree ==================== root R Parent Child Parent ============================================== liblarch-3.2.0/examples/cycle2000066400000000000000000000002431420425305200162120ustar00rootroot00000000000000Tree before operation ==================== Tree ==================== root R A_node B_node B_node A_node ============================================== liblarch-3.2.0/examples/delete-randomly000066400000000000000000000015721420425305200201240ustar00rootroot00000000000000Tree before operation ==================== Tree ==================== root 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ============================================== Tasks should be removed in this order: ['22', '42', '37', '35', '32', '16', '8', '20', '27', '25', '3', '17', '19', '38', '12', '15', '9', '10', '30', '33', '13', '24', '28', '2', '23', '7', '4', '39', '1', '11', '26', '29', '6', '31', '14', '40', '34', '41', '21', '18', '36', '5'] ==================== Tree ==================== root ============================================== Tree after operation ==================== Tree ==================== root ============================================== liblarch-3.2.0/examples/test_suite000066400000000000000000000002501420425305200172170ustar00rootroot00000000000000Tree before operation ==================== Tree ==================== root A X B X C D X X E F X X ============================================== liblarch-3.2.0/examples/torture000066400000000000000000000003211420425305200165320ustar00rootroot00000000000000Tree before operation ==================== Tree ==================== root 0 1 2 3 4 5 parent 10 11 12 13 14 11 12 13 14 ============================================== liblarch-3.2.0/liblarch/000077500000000000000000000000001420425305200150515ustar00rootroot00000000000000liblarch-3.2.0/liblarch/__init__.py000066400000000000000000000150621420425305200171660ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- from .filters_bank import FiltersBank from .tree import MainTree from .treenode import _Node from .viewcount import ViewCount from .viewtree import ViewTree # API version of liblarch. # Your application is compatible if the major version number match liblarch's # one and if your minor version number is inferior to liblarch's one. # # The major number is incremented if an existing method is removed or modified # The minor number is incremented when a method is added to the API API = "3.0" def is_compatible(request): major, minor = [int(i) for i in request.split(".")] current_ma, current_mi = [int(i) for i in API.split(".")] return major == current_ma and minor <= current_mi class TreeNode(_Node): """ The public interface for TreeNode """ def __init__(self, node_id, parent=None): _Node.__init__(self, node_id, parent) def _set_tree(tree): print("_set_tree is not part of the API") class Tree(object): """ A thin wrapper to MainTree that adds filtering capabilities. It also provides a few methods to operate complex operation on the MainTree (e.g, move_node) """ def __init__(self): """ Creates MainTree which wraps and a main view without filters """ self.__tree = MainTree() self.__fbank = FiltersBank(self.__tree) self.__views = {} self.__viewscount = {} self.__views['main'] = ViewTree( self, self.__tree, self.__fbank, static=True) # HANDLE NODES ############################################################ def get_node(self, node_id): """ Returns the object of node. If the node does not exists, a ValueError is raised. """ return self.__tree.get_node(node_id) def has_node(self, node_id): """ Does the node exists in this tree? """ return self.__tree.has_node(node_id) def add_node(self, node, parent_id=None, priority="low"): """ Add a node to tree. If parent_id is set, put the node as a child of this node, otherwise put it as a child of the root node.""" self.__tree.add_node(node, parent_id, priority) def del_node(self, node_id, recursive=False): """ Remove node from tree and return whether it was successful or not """ return self.__tree.remove_node(node_id, recursive) def refresh_node(self, node_id, priority="low"): """ Send a request for updating the node """ self.__tree.modify_node(node_id, priority) def refresh_all(self): """ Refresh all nodes """ self.__tree.refresh_all() def move_node(self, node_id, new_parent_id=None): """ Move the node to a new parent (dismissing all other parents) use pid None to move it to the root """ if self.has_node(node_id): node = self.get_node(node_id) node.set_parent(new_parent_id) toreturn = True else: toreturn = False return toreturn def add_parent(self, node_id, new_parent_id=None): """ Add the node to a new parent. Return whether operation was successful or not. If the node does not exists, return False """ if self.has_node(node_id): node = self.get_node(node_id) return node.add_parent(new_parent_id) else: return False # VIEWS ################################################################### def get_main_view(self): """ Return the special view "main" which is without any filters on it. """ return self.__views['main'] def get_viewtree(self, name=None, refresh=True): """ Returns a viewtree by the name: * a viewtree with that name exists => return it * a viewtree with that name does not exist => create a new one and return it * name is None => create an anonymous tree (do not remember it) If refresh is False, the view is not initialized. This is useful as an optimization if you plan to apply a filter. """ if name is not None and name in self.__views: view_tree = self.__views[name] else: view_tree = ViewTree( self, self.__tree, self.__fbank, name=name, refresh=refresh) if name is not None: self.__views[name] = view_tree return view_tree def get_viewcount(self, name=None, refresh=True): if name is not None and name in self.__viewscount: view_count = self.__viewscount[name] else: view_count = ViewCount( self.__tree, self.__fbank, name=name, refresh=refresh) if name is not None: self.__viewscount[name] = view_count return view_count # FILTERS ################################################################# def list_filters(self): """ Return a list of all available filters by name """ return self.__fbank.list_filters() def add_filter(self, filter_name, filter_func, parameters=None): """ Adds a filter to the filter bank. @filter_name : name to give to the filter @filter_func : the function that will filter the nodes @parameters : some default parameters fot that filter Return True if the filter was added Return False if the filter_name was already in the bank """ return self.__fbank.add_filter(filter_name, filter_func, parameters) def remove_filter(self, filter_name): """ Remove a filter from the bank. Only custom filters that were added here can be removed. Return False if the filter was not removed. """ return self.__fbank.remove_filter(filter_name) liblarch-3.2.0/liblarch/filteredtree.py000066400000000000000000000604171420425305200201110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- from gi.repository import GObject class FilteredTree(object): """ FilteredTree is the most important and also the most buggy part of LibLarch. FilteredTree transforms general changes in tree like creating/removing relationships between nodes and adding/updating/removing nodes into a serie of simple steps which can be for instance by GTK Widget. FilteredTree allows filtering - hiding certain nodes defined by a predicate. The reason of most bugs is that FilteredTree is request to update a node. FilteredTree must update its ancestors and also decestors. You cann't do that by a simple recursion. """ def __init__(self, tree, filtersbank, name=None, refresh=True): """ Construct a layer where filters could by applied @param tree: Original tree to filter. @param filtersbank: Filter bank which stores filters @param refresh: Requests all nodes in the beginning? Additional filters can be added and refresh can be done later _flat defines whether only nodes without children can be shown. For example WorkView filter. """ self.cllbcks = {} # Cache self.nodes = {} # Set root_id by the name of FilteredTree if name is None: self.root_id = "anonymous_root" else: self.root_id = "root_%s" % name self.nodes[self.root_id] = {'parents': [], 'children': []} self.cache_paths = {} self.filter_cache = {} # Connect to signals from MainTree self.tree = tree self.tree.register_callback("node-added", self.__external_modify) self.tree.register_callback("node-modified", self.__external_modify) self.tree.register_callback("node-deleted", self.__external_modify) # Filters self.__flat = False self.applied_filters = [] self.fbank = filtersbank if refresh: self.refilter() def set_callback(self, event, func, node_id=None, param=None): """ Register a callback for an event. It is possible to have just one callback for event. @param event: one of added, modified, deleted, reordered @param func: callback function """ if event == 'runonce': if not node_id: raise Exception('runonce callback should come with a node_id') if self.is_displayed(node_id): # it is essential to idle_add to avoid hard recursion GObject.idle_add(func, param) else: if node_id not in self.cllbcks: self.cllbcks[node_id] = [] self.cllbcks[node_id].append([func, node_id, param]) else: self.cllbcks[event] = [func, node_id, param] def callback(self, event, node_id, path, neworder=None): """ Run a callback. To call callback, the object must be initialized and function exists. @param event: one of added, modified, deleted, reordered, runonce @param node_id: node_id parameter for callback function @param path: path parameter for callback function @param neworder: neworder parameter for reorder callback function The runonce event is actually only run once, when a given task appears. """ if event == 'added': for func, nid, param in self.cllbcks.get(node_id, []): if nid and self.is_displayed(nid): func(param) if node_id in self.cllbcks: self.cllbcks.pop(node_id) else: raise Exception( '{} is not displayed but {} was added'.format( nid, node_id)) func, nid, param = self.cllbcks.get(event, (None, None, None)) if func: if neworder: func(node_id, path, neworder) else: func(node_id, path) # EXTERNAL MODIFICATION ################################################### def __external_modify(self, node_id): return self.__update_node(node_id, direction="both") def __update_node(self, node_id, direction): '''update the node node_id and propagate the change in direction (up|down|both) ''' if node_id == self.root_id: return None current_display = self.is_displayed(node_id) new_display = self.__is_displayed(node_id) for fcname in self.filter_cache: if node_id in self.filter_cache[fcname]['nodes']: self.filter_cache[fcname]['nodes'].remove(node_id) self.filter_cache[fcname]['count'] = len( self.filter_cache[fcname]['nodes']) completely_updated = True if not current_display and not new_display: # If a task is not displayed and should not be displayed, we # should still check its parent because he might not be aware # that he has a child if self.tree.has_node(node_id): node = self.tree.get_node(node_id) for parent in node.get_parents(): self.__update_node(parent, "up") return completely_updated elif not current_display and new_display: action = 'added' elif current_display and not new_display: action = 'deleted' else: action = 'modified' # Create node info for new node if action == 'added': self.nodes[node_id] = {'parents': [], 'children': []} # Make sure parents are okay if we adding or updating if action == 'added' or action == 'modified': current_parents = self.nodes[node_id]['parents'] new_parents = self.__node_parents(node_id) # When using flat filter or a recursive filter, FilteredTree # might not recognize a parent correctly, make sure to check them if action == 'added': node = self.tree.get_node(node_id) for parent_id in node.get_parents(): if (parent_id not in new_parents and parent_id not in current_parents): self.__update_node(parent_id, direction="up") # Refresh list of parents after doing checkup once again current_parents = self.nodes[node_id]['parents'] new_parents = self.__node_parents(node_id) self.nodes[node_id]['parents'] = [ parent_id for parent_id in new_parents if parent_id in self.nodes] remove_from = set(current_parents) - set(new_parents) add_to = set(new_parents) - set(current_parents) stay = set(new_parents) - set(add_to) # If we are updating a node at the root, we should take care # of the root too if direction == "down" and self.root_id in add_to: direction = "both" for parent_id in remove_from: self.send_remove_tree(node_id, parent_id) self.nodes[parent_id]['children'].remove(node_id) if direction == "both" or direction == "up": self.__update_node(parent_id, direction="up") # there might be some optimization here for parent_id in add_to: if parent_id in self.nodes: self.nodes[parent_id]['children'].append(node_id) self.send_add_tree(node_id, parent_id) if direction == "both" or direction == "up": self.__update_node(parent_id, direction="up") else: completely_updated = False raise Exception("We have a parent not in the ViewTree") # We update all the other parents if direction == "both" or direction == "up": for parent_id in stay: self.__update_node(parent_id, direction="up") # We update the node itself # Why should we call the callback only for modify? if action == 'modified': for path in self.get_paths_for_node(node_id): self.callback(action, node_id, path) # We update the children current_children = self.nodes[node_id]['children'] new_children = self.__node_children(node_id) if direction == "both" or direction == "down": for cid in new_children: if cid not in current_children: self.__update_node(cid, direction="down") elif action == 'deleted': paths = self.get_paths_for_node(node_id) children = list(reversed(self.nodes[node_id]['children'])) for child_id in children: self.send_remove_tree(child_id, node_id) self.nodes[child_id]['parents'].remove(node_id) self.__update_node(child_id, direction="down") node = self.nodes.pop(node_id) for path in paths: self.callback(action, node_id, path) # Remove node from cache for parent_id in node['parents']: self.nodes[parent_id]['children'].remove(node_id) self.__update_node(parent_id, direction="up") # We update parents who are not displayed # If the node is only hidden and still exists in the tree if self.tree.has_node(node_id): node = self.tree.get_node(node_id) for parent in node.get_parents(): if parent not in self.nodes: self.__update_node(parent, direction="up") return completely_updated def send_add_tree(self, node_id, parent_id): paths = self.get_paths_for_node(parent_id) queue = [(node_id, (node_id, ))] while queue != []: node_id, relative_path = queue.pop(0) for start_path in paths: path = start_path + relative_path self.callback('added', node_id, path) for child_id in self.nodes[node_id]['children']: queue.append((child_id, relative_path + (child_id, ))) def send_remove_tree(self, node_id, parent_id): paths = self.get_paths_for_node(parent_id) stack = [(node_id, (node_id, ), True)] while stack != []: node_id, relative_path, first_time = stack.pop() if first_time: stack.append((node_id, relative_path, False)) for child_id in self.nodes[node_id]['children']: stack.append( (child_id, relative_path + (child_id, ), True)) else: for start_path in paths: path = start_path + relative_path self.callback('deleted', node_id, path) def test_validity(self): for node_id in self.nodes: for parent_id in self.nodes[node_id]['parents']: assert node_id in self.nodes[parent_id]['children'], ( "Node '{}' is not in children of '{}'".format( node_id, parent_id)) if self.nodes[node_id]['parents'] == []: assert node_id == self.root_id, ( "Node '{}' does not have parents".format(node_id)) for parent_id in self.nodes[node_id]['children']: assert node_id in self.nodes[parent_id]['parents'], ( "Node '{}' is not in parents of '{}'".format( node_id, parent_id)) # OTHER ################################################################### def refilter(self): # Find out it there is at least one flat filter self.filter_cache = {} self.__flat = False for filter_name in self.applied_filters: filt = self.fbank.get_filter(filter_name) if filt and filt.is_flat(): self.__flat = True break # Clean the tree for node_id in reversed(self.nodes[self.root_id]['children']): self.send_remove_tree(node_id, self.root_id) self.nodes = {self.root_id: {'parents': [], 'children': []}} # Build tree again root_node = self.tree.get_root() queue = root_node.get_children() while queue: node_id = queue.pop(0) # FIXME: decide which is the best direction self.__update_node(node_id, direction="both") queue.extend(self.tree.get_node(node_id).get_children()) def __is_displayed(self, node_id): """ Should be node displayed regardless of its current status? """ if node_id and self.tree.has_node(node_id): for filter_name in self.applied_filters: filt = self.fbank.get_filter(filter_name) if filt: can_be_displayed = filt.is_displayed(node_id) if not can_be_displayed: return False else: return False return True else: return False def is_displayed(self, node_id): """ Is the node displayed at the moment? """ return node_id in self.nodes def __node_children(self, node_id): if node_id == self.root_id: raise Exception("Requesting children for root node") if not self.__flat: if self.tree.has_node(node_id): node = self.tree.get_node(node_id) else: node = None else: node = None if not node: return [] toreturn = [] for child_id in node.get_children(): if self.__is_displayed(child_id): toreturn.append(child_id) return toreturn def __node_parents(self, node_id): """ Returns parents of the given node. If node has no parent or no displyed parent, return the virtual root. """ if node_id == self.root_id: raise ValueError("Requested a parent of the root node") parents_nodes = [] # we return only parents that are not root and displayed if not self.__flat and self.tree.has_node(node_id): node = self.tree.get_node(node_id) for parent_id in node.get_parents(): if parent_id in self.nodes and self.__is_displayed(parent_id): parents_nodes.append(parent_id) # Add to root if it is an orphan if parents_nodes == []: parents_nodes = [self.root_id] return parents_nodes # This is a crude hack which is more performant that other methods def is_path_valid(self, p): valid = True i = 0 if len(p) == 1: valid = False else: while valid and i < len(p) - 1: child = p[i + 1] par = p[i] if par in self.nodes: valid = (child in self.nodes[par]['children']) else: valid = False i += 1 return valid def get_paths_for_node(self, node_id): if node_id == self.root_id or not self.is_displayed(node_id): return [()] else: toreturn = [] for parent_id in self.nodes[node_id]['parents']: if parent_id not in self.nodes: raise Exception("Parent %s does not exists" % parent_id) if node_id not in self.nodes[parent_id]['children']: # Dump also state of FilteredTree => useful for debugging s = "\nCurrent tree:\n" for key in self.nodes: s += "{}\n\t parents: {}\n\t children: {}\n".format( key, str(self.nodes[key]['parents']), str(self.nodes[key]['children'])) raise Exception( "{} is not children of {}\n{}".format( node_id, parent_id, s)) for parent_path in self.get_paths_for_node(parent_id): mypath = parent_path + (node_id, ) toreturn.append(mypath) self.cache_paths[node_id] = toreturn return toreturn def print_tree(self, string=False): """ Representation of tree in FilteredTree @param string: if set, instead of printing, return string for printing. """ stack = [(self.root_id, "")] output = "_" * 30 + "\n" + "FilteredTree cache\n" + "_" * 30 + "\n" while stack != []: node_id, prefix = stack.pop() output += prefix + str(node_id) + '\n' for child_id in reversed(self.nodes[node_id]['children']): stack.append((child_id, prefix + " ")) output += "_" * 30 + "\n" if string: return output else: print(output) def get_all_nodes(self): nodes = list(self.nodes.keys()) nodes.remove(self.root_id) return nodes def get_n_nodes(self, withfilters=[]): """ returns quantity of displayed nodes in this tree if the withfilters is set, returns the quantity of nodes that will be displayed if we apply those filters to the current tree. It means that the currently applied filters are also taken into account. """ return len(self.get_nodes(withfilters=withfilters)) def get_nodes(self, withfilters=[]): """ returns quantity of displayed nodes in this tree if the withfilters is set, returns the quantity of nodes that will be displayed if we apply those filters to the current tree. It means that the currently applied filters are also taken into account. """ if withfilters == []: # Use current cache return self.get_all_nodes() elif len(withfilters) == 1 and withfilters[0] in self.filter_cache: return self.filter_cache[withfilters[0]]['nodes'] else: # Filter on the current nodes filters = [] for filter_name in withfilters: filt = self.fbank.get_filter(filter_name) if filt: filters.append(filt) nodes = [] for node_id in self.nodes: if node_id == self.root_id: continue displayed = True for filt in filters: displayed = filt.is_displayed(node_id) if not displayed: break if displayed: nodes.append(node_id) return nodes def get_node_for_path(self, path): if not path or path == (): return None node_id = path[-1] # Both "if" should be benchmarked if path in self.get_paths_for_node(node_id): return node_id else: return None def next_node(self, node_id, parent_id): if node_id == self.root_id: raise Exception("Calling next_node on the root node") if node_id not in self.nodes: raise Exception("Node %s is not displayed" % node_id) parents = self.nodes[node_id]['parents'] if not parent_id: parent_id = parents[0] elif parent_id not in parents: raise Exception( "Node {} does not have parent {}".format(node_id, parent_id)) index = self.nodes[parent_id]['children'].index(node_id) if index + 1 < len(self.nodes[parent_id]['children']): return self.nodes[parent_id]['children'][index + 1] else: return None def node_all_children(self, node_id=None): if node_id is None: node_id = self.root_id return list(self.nodes[node_id]['children']) def node_has_child(self, node_id): return len(self.nodes[node_id]['children']) > 0 def node_n_children(self, node_id, recursive=False): if node_id is None: node_id = self.root_id if node_id not in self.nodes: return 0 if recursive: total = 0 # We avoid recursion in a loop # because the dict might be updated in the meantime cids = list(self.nodes[node_id]['children']) for cid in cids: total += self.node_n_children(cid, recursive=True) total += 1 # we count the node itself ofcourse return total else: return len(self.nodes[node_id]['children']) def node_nth_child(self, node_id, n): return self.nodes[node_id]['children'][n] def node_parents(self, node_id): if node_id not in self.nodes: raise IndexError('Node %s is not displayed' % node_id) parents = list(self.nodes[node_id]['parents']) if self.root_id in parents: parents.remove(self.root_id) return parents def get_current_state(self): """ Allows to connect LibLarch widget on fly to FilteredTree Sends 'added' signal/callback for every nodes that is currently in FilteredTree. After that, FilteredTree and TreeModel are in the same state """ for node_id in self.nodes[self.root_id]['children']: self.send_add_tree(node_id, self.root_id) # FILTERS ################################################################# def list_applied_filters(self): return list(self.applied_filters) def apply_filter(self, filter_name, parameters=None, reset=None, refresh=True): """ Apply a new filter to the tree. @param filter_name: The name of an registrered filter from filters_bank @param parameters: Optional parameters to pass to the filter @param reset: Should other filters be removed? @param refresh: Should be refereshed the whole tree? (performance optimization) """ should_refilter = False if reset: self.applied_filters = [] if parameters: filt = self.fbank.get_filter(filter_name) if filt: filt.set_parameters(parameters) should_refilter = True else: raise ValueError( "No filter of name {} in the bank".format(filter_name)) if filter_name not in self.applied_filters: self.applied_filters.append(filter_name) should_refilter = True toreturn = True else: toreturn = False if refresh and should_refilter: self.refilter() return toreturn def unapply_filter(self, filter_name, refresh=True): """ Removes a filter from the tree. @param filter_name: The name of an already added filter to remove @param refresh: Should be refereshed the whole tree? (performance optimization) """ if filter_name in self.applied_filters: self.applied_filters.remove(filter_name) if refresh: self.refilter() return True else: return False def reset_filters(self, refresh=True): """ Clears all filters currently set on the tree. Can't be called on the main tree. """ self.applied_filters = [] if refresh: self.refilter() liblarch-3.2.0/liblarch/filters_bank.py000066400000000000000000000101141420425305200200630ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- """ filters_bank stores all of GTG's filters in centralized place """ class Filter(object): def __init__(self, func, req): self.func = func self.dic = {} self.tree = req def set_parameters(self, dic): if dic: self.dic = dic def is_displayed(self, node_id): if self.tree.has_node(node_id): task = self.tree.get_node(node_id) else: return False if self.dic: value = self.func(task, parameters=self.dic) else: value = self.func(task) if 'negate' in self.dic and self.dic['negate']: value = not value return value def get_parameters(self, param): return self.dic.get(param, None) def is_flat(self): """ Should be the final list flat """ return self.get_parameters('flat') class FiltersBank(object): """ Stores filter objects in a centralized place. """ def __init__(self, tree): """ Create several stock filters: workview - Tasks that are active, workable, and started active - Tasks of status Active closed - Tasks of status closed or dismissed notag - Tasks with no tags """ self.tree = tree self.available_filters = {} self.custom_filters = {} ########################################## def get_filter(self, filter_name): """ Get the filter object for a given name """ if filter_name in self.available_filters: return self.available_filters[filter_name] elif filter_name in self.custom_filters: return self.custom_filters[filter_name] else: return None def has_filter(self, filter_name): return filter_name in self.available_filters \ or filter_name in self.custom_filters def list_filters(self): """ List, by name, all available filters """ liste = list(self.available_filters.keys()) liste += list(self.custom_filters.keys()) return liste def add_filter(self, filter_name, filter_func, parameters=None): """ Adds a filter to the filter bank Return True if the filter was added Return False if the filter_name was already in the bank """ if filter_name not in self.list_filters(): if filter_name.startswith('!'): filter_name = filter_name[1:] else: filter_obj = Filter(filter_func, self.tree) filter_obj.set_parameters(parameters) self.custom_filters[filter_name] = filter_obj return True else: return False def remove_filter(self, filter_name): """ Remove a filter from the bank. Only custom filters that were added here can be removed Return False if the filter was not removed """ if filter_name not in self.available_filters: if filter_name in self.custom_filters: self.custom_filters.pop(filter_name) return True else: return False else: return False liblarch-3.2.0/liblarch/processqueue.py000066400000000000000000000075441420425305200201600ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- import threading from gi.repository import GObject class SyncQueue(object): """ Synchronized queue for processing requests""" def __init__(self): """ Initialize synchronized queue. @param callback - function for processing requests""" self._low_queue = [] self._queue = [] self._vip_queue = [] self._handler = None self._lock = threading.Lock() self._origin_thread = threading.current_thread() self.count = 0 def process_queue(self): """ Process requests from queue """ for action in self.process(): func = action[0] func(*action[1:]) # return True to process other requests as well return True def push(self, *element, **kwargs): """ Add a new element to the queue. Process actions from the same thread as the thread which created this queue immediately. What does it mean? When I use liblarch without threads, all actions are processed immediately. In GTG, this queue is created by the main thread which process GUI. When GUI callback is triggered, process those actions immediately because no protection is needed. However, requests from synchronization services are put in the queue. Application can choose which kind of priority should have an update. If the request is not in the queue of selected priority, add it and setup callback. """ if self._origin_thread == threading.current_thread(): func = element[0] func(*element[1:]) return priority = kwargs.get('priority') if priority == 'low': queue = self._low_queue elif priority == 'high': queue = self._vip_queue else: queue = self._queue self._lock.acquire() if element not in queue: queue.append(element) if self._handler is None: self._handler = GObject.idle_add(self.process_queue) self._lock.release() def process(self): """ Return elements to process At the moment, it returns just one element. In the future more elements may be better to return (to speed it up). If there is no request left, disable processing. """ self._lock.acquire() if len(self._vip_queue) > 0: toreturn = [self._vip_queue.pop(0)] elif len(self._queue) > 0: toreturn = [self._queue.pop(0)] elif len(self._low_queue) > 0: toreturn = [self._low_queue.pop(0)] else: toreturn = [] if (len(self._queue) == 0 and len(self._vip_queue) == 0 and len(self._low_queue) == 0 and self._handler is not None): GObject.source_remove(self._handler) self._handler = None self._lock.release() return toreturn liblarch-3.2.0/liblarch/tree.py000066400000000000000000000347741420425305200164010ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- from . import processqueue from .treenode import _Node class MainTree(object): """ Tree which stores and handle all requests """ def __init__(self): """ Initialize MainTree. @param root - the "root" node which contains all nodes """ self.nodes = {} self.pending_relationships = [] self.__cllbcks = {} self.root_id = 'root' self.root = _Node(self.root_id) _Node._set_tree(self.root, self) self._queue = processqueue.SyncQueue() def __str__(self): return "" % self.root def get_root(self): """ Return root node """ return self.root # INTERFACE FOR CALLBACKS ################################################# def register_callback(self, event, func): """ Store function and return unique key which can be used to unregister the callback later """ if event not in self.__cllbcks: self.__cllbcks[event] = {} callbacks = self.__cllbcks[event] key = 0 while key in callbacks: key += 1 callbacks[key] = func return key def deregister_callback(self, event, key): """ Remove the callback identifed by key (from register_cllbck) """ try: del self.__cllbcks[event][key] except KeyError: pass def _callback(self, event, node_id): """ Inform others about the event """ # We copy the dict to not loop on it while it could be modified dic = dict(self.__cllbcks.get(event, {})) for func in dic.values(): func(node_id) # INTERFACE FOR HANDLING REQUESTS ######################################### def add_node(self, node, parent_id=None, priority="low"): self._queue.push(self._add_node, node, parent_id, priority=priority) def remove_node(self, node_id, recursive=False): self._queue.push(self._remove_node, node_id, recursive) def modify_node(self, node_id, priority="low"): self._queue.push(self._modify_node, node_id, priority=priority) def new_relationship(self, parent_id, child_id): self._queue.push(self._new_relationship, parent_id, child_id) def break_relationship(self, parent_id, child_id): self._queue.push(self._break_relationship, parent_id, child_id) def refresh_all(self): """ Refresh all nodes """ for node_id in list(self.nodes.keys()): self.modify_node(node_id) # IMPLEMENTATION OF HANDLING REQUESTS ##################################### def _create_relationship(self, parent_id, child_id): """ Create relationship without any checks """ parent = self.nodes[parent_id] child = self.nodes[child_id] if child_id not in parent.children: parent.children.append(child_id) if parent_id not in child.parents: child.parents.append(parent_id) if child_id in self.root.children: self.root.children.remove(child_id) def _destroy_relationship(self, parent_id, child_id): """ Destroy relationship without any checks """ parent = self.nodes[parent_id] child = self.nodes[child_id] if child_id in parent.children: parent.children.remove(child_id) if parent_id in child.parents: child.parents.remove(parent_id) def _is_circular_relation(self, parent_id, child_id): """ Would the new relation be circular? Go over every possible ancestors. If one of them is child_id, this would be circular relation. """ visited = [] ancestors = [parent_id] while ancestors != []: node_id = ancestors.pop(0) if node_id == child_id: return True if node_id not in self.nodes: continue for ancestor_id in self.nodes[node_id].parents: if ancestor_id not in visited: ancestors.append(ancestor_id) return False def _add_node(self, node, parent_id): """ Add a node to the tree @param node - node to be added @param parent_id - parent to add or it will be add to root """ node_id = node.get_id() if node_id in self.nodes: print("Error: Node '%s' already exists" % node_id) return False _Node._set_tree(node, self) for relationship in node.pending_relationships: if relationship not in self.pending_relationships: self.pending_relationships.append(relationship) node.pending_relationships = [] self.nodes[node_id] = node add_to_root = True parents_to_refresh = [] children_to_refresh = [] # Build pending relationships for rel_parent_id, rel_child_id in list(self.pending_relationships): # Adding as a child if rel_child_id == node_id and rel_parent_id in self.nodes: if not self._is_circular_relation(rel_parent_id, node_id): self._create_relationship(rel_parent_id, node_id) add_to_root = False parents_to_refresh.append(rel_parent_id) else: print("Error: Detected pending circular relationship", rel_parent_id, rel_child_id) self.pending_relationships.remove( (rel_parent_id, rel_child_id)) # Adding as a parent if rel_parent_id == node_id and rel_child_id in self.nodes: if not self._is_circular_relation(node_id, rel_child_id): self._create_relationship(node_id, rel_child_id) children_to_refresh.append(rel_child_id) else: print("Error: Detected pending circular relationship", rel_parent_id, rel_child_id) self.pending_relationships.remove((rel_parent_id, rel_child_id)) # Build relationship with given parent if parent_id is not None: if self._is_circular_relation(parent_id, node_id): raise Exception( 'Creating circular relationship between {} and {}'.format( parent_id, node_id)) if parent_id in self.nodes: self._create_relationship(parent_id, node_id) add_to_root = False parents_to_refresh.append(parent_id) else: self.pending_relationships.append((parent_id, node_id)) # Add at least to root if add_to_root: self.root.children.append(node_id) # Send callbacks # updating the parent and the children is handled by the FT self._callback("node-added", node_id) def _remove_node(self, node_id, recursive=False): """ Remove node from tree """ if node_id not in self.nodes: print("*** Warning *** Trying to remove a non-existing node") return # Do not remove root node if node_id is None: return # Remove pending relationships with this node for relation in list(self.pending_relationships): if node_id in relation: self.pending_relationships.remove(relation) node = self.nodes[node_id] # Handle parents for parent_id in node.parents: self._destroy_relationship(parent_id, node_id) self._callback('node-modified', parent_id) # Handle children for child_id in list(node.children): if recursive: self._remove_node(child_id, True) else: self._destroy_relationship(node_id, child_id) self._callback('node-modified', child_id) if self.nodes[child_id].parents == []: self.root.children.append(child_id) if node_id in self.root.children: self.root.children.remove(node_id) self.nodes.pop(node_id) self._callback('node-deleted', node_id) def _modify_node(self, node_id): """ Force update of a node """ if node_id != self.root_id and node_id in self.nodes: self._callback('node-modified', node_id) def _new_relationship(self, parent_id, child_id): """ Creates a new relationship This method is used mainly from TreeNode""" if (parent_id, child_id) in self.pending_relationships: self.pending_relationships.remove((parent_id, child_id)) if not parent_id or not child_id or parent_id == child_id: return False if parent_id not in self.nodes or child_id not in self.nodes: self.pending_relationships.append((parent_id, child_id)) return True if self._is_circular_relation(parent_id, child_id): self._destroy_relationship(parent_id, child_id) raise Exception( 'Cannot build circular relationship between {} and {}'.format( parent_id, child_id)) self._create_relationship(parent_id, child_id) # Remove from root when having a new relationship if child_id in self.root.children: self.root.children.remove(child_id) self._callback('node-modified', parent_id) self._callback('node-modified', child_id) def _break_relationship(self, parent_id, child_id): """ Remove a relationship This method is used mainly from TreeNode """ for rel_parent, rel_child in list(self.pending_relationships): if rel_parent == parent_id and rel_child == child_id: self.pending_relationships.remove((rel_parent, rel_child)) if not parent_id or not child_id or parent_id == child_id: return False if parent_id not in self.nodes or child_id not in self.nodes: return False self._destroy_relationship(parent_id, child_id) # Move to root if beak the last parent if self.nodes[child_id].get_parents() == []: self.root.add_child(child_id) self._callback('node-modified', parent_id) self._callback('node-modified', child_id) # INTERFACE FOR READING STATE OF TREE ##################################### def has_node(self, node_id): """ Is this node_id in this tree? """ return node_id in self.nodes def get_node(self, node_id=None): """ Return node of tree or root node of this tree """ if node_id in self.nodes: return self.nodes[node_id] elif node_id == self.root_id or node_id is None: return self.root else: raise ValueError("Node %s is not in the tree" % node_id) def get_node_for_path(self, path): """ Convert path into node_id @return node_id if path is valid, None otherwise """ if not path or path == (): return None node_id = path[-1] if path in self.get_paths_for_node(node_id): return node_id else: return None return node_id def get_paths_for_node(self, node_id): """ Get all paths for node_id """ if not node_id or node_id == self.root_id: return [()] elif node_id in self.nodes: node = self.nodes[node_id] if node.has_parent(): paths = [] for parent_id in node.get_parents(): if parent_id not in self.nodes: continue for path in self.get_paths_for_node(parent_id): paths.append(path + (node_id, )) return paths else: return [(node_id, )] else: raise ValueError("Cannot get path for non existing node {}".format( node_id)) def get_all_nodes(self): """ Return list of all nodes in this tree """ return list(self.nodes.keys()) def next_node(self, node_id, parent_id=None): """ Return the next sibling node or None if there is none @param node_id - we look for siblings of this node @param parent_id - specify which siblings should be used, if task has more parents. If None, random parent will be used """ if node_id is None: raise ValueError('node_id should be different than None') node = self.get_node(node_id) parents_id = node.get_parents() if len(parents_id) == 0: parid = self.root_id elif parent_id in parents_id: parid = parent_id else: parid = parents_id[0] parent = self.get_node(parid) if not parent: raise ValueError('Parent does not exist') index = parent.get_child_index(node_id) if index is None: error = 'children are : {}\n'.format(parent.get_children()) error += 'node {} is not a child of {}'.format(node_id, parid) raise IndexError(error) if parent.get_n_children() > index + 1: return parent.get_nth_child(index + 1) else: return None def print_tree(self, string=False): output = self.root_id + "\n" stack = [(" ", child_id) for child_id in reversed(self.root.children)] while stack != []: prefix, node_id = stack.pop() output += prefix + node_id + "\n" prefix += " " for child_id in reversed(self.nodes[node_id].get_children()): stack.append((prefix, child_id)) if string: return output else: print(output, end=' ') liblarch-3.2.0/liblarch/treenode.py000066400000000000000000000172421420425305200172360ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- class _Node(object): """ Object just for a single node in Tree """ def __init__(self, node_id, parent=None): """ Initializes node @param node_id - unique identifier of node (str) @param parent - node_id of parent """ self.node_id = node_id self.parents_enabled = True self.children_enabled = True self.parents = [] self.children = [] self.tree = None self.pending_relationships = [] if parent: self.add_parent(parent) def __str__(self): return "" % (self.node_id) def get_id(self): """ Return node_id """ return self.node_id def modified(self, priority="low"): """ Force to update node (because it has changed) """ if self.tree: self.tree.modify_node(self.node_id, priority=priority) def _set_tree(self, tree): """ Set tree which is should contain this node. This method should be called only from MainTree. It is not part of public interface. """ self.tree = tree def get_tree(self): """ Return associated tree with this node """ return self.tree # Parents ################################################################# def set_parents_enabled(self, bol): if not bol: for p in self.get_parents(): self.remove_parent(p) self.parents_enabled = bol def has_parents_enabled(self): return self.parents_enabled def add_parent(self, parent_id): """ Add a new parent """ if (parent_id != self.get_id() and self.parents_enabled and parent_id not in self.parents): if not self.tree: self.pending_relationships.append((parent_id, self.get_id())) elif not self.tree.has_node(parent_id): self.tree.pending_relationships.append( (parent_id, self.get_id())) else: par = self.tree.get_node(parent_id) if par.has_children_enabled(): self.tree.new_relationship(parent_id, self.node_id) def set_parent(self, parent_id): """ Remove other parents and set this parent as only parent """ if parent_id != self.get_id() and self.parents_enabled: is_already_parent_flag = False if not self.tree: self.pending_relationships.append((parent_id, self.get_id())) elif not self.tree.has_node(parent_id): for p in self.get_parents(): self.tree.break_relationship(p, self.get_id()) self.tree.pending_relationships.append( (parent_id, self.get_id())) else: par = self.tree.get_node(parent_id) if par.has_children_enabled(): # First we remove all the other parents for node_id in self.parents: if node_id != parent_id: self.remove_parent(node_id) else: is_already_parent_flag = True if parent_id and not is_already_parent_flag: self.add_parent(parent_id) def remove_parent(self, parent_id): """ Remove parent """ if self.parents_enabled and parent_id in self.parents: self.parents.remove(parent_id) self.tree.break_relationship(parent_id, self.node_id) def has_parent(self, parent_id=None): """ Has parent/parents? @param parent_id - None => has any parent? not None => has this parent? """ if self.parents_enabled: if parent_id: parent_in_tree = self.tree.has_node(parent_id) own_parent = parent_id in self.parents return parent_in_tree and own_parent else: return len(self.parents) > 0 else: return False def get_parents(self): """ Return parents of node """ parents = [] if self.parents_enabled and self.tree: for parent_id in self.parents: if self.tree.has_node(parent_id): parents.append(parent_id) return parents # Children ################################################################ def set_children_enabled(self, bol): if not bol: for c in self.get_children(): self.tree.break_relationship(self.get_id(), c) self.children_enabled = bol def has_children_enabled(self): return self.children_enabled def add_child(self, child_id): """ Add a children to node """ if self.children_enabled and child_id != self.get_id(): if child_id not in self.children: if not self.tree: self.pending_relationships.append( (self.get_id(), child_id)) elif not self.tree.has_node(child_id): self.tree.pending_relationships.append( (self.get_id(), child_id)) else: child = self.tree.get_node(child_id) if child.has_parents_enabled(): self.children.append(child_id) self.tree.new_relationship(self.node_id, child_id) else: print("{} was already in children of {}".format( child_id, self.node_id)) def has_child(self, child_id=None): """ Has child/children? @param child_id - None => has any child? not None => has this child? """ if self.children_enabled: if child_id: return child_id in self.children else: return bool(self.children) else: return False def get_children(self): """ Return children of nodes """ children = [] if self.children_enabled and self.tree: for child_id in self.children: if self.tree.has_node(child_id): children.append(child_id) return children def get_n_children(self): """ Return count of children """ if self.children_enabled: return len(self.get_children()) else: return 0 def get_nth_child(self, index): """ Return nth child """ try: return self.children[index] except(IndexError): raise ValueError("Requested non-existing child") def get_child_index(self, node_id): if self.children_enabled and node_id in self.children: return self.children.index(node_id) else: return None liblarch-3.2.0/liblarch/viewcount.py000066400000000000000000000067741420425305200174640ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # --------------------------------------------------------------------------- class ViewCount(object): def __init__(self, tree, fbank, name=None, refresh=True): self.initialized = False self.ncount = {} self.tree = tree self.tree.register_callback("node-added", self.__modify) self.tree.register_callback("node-modified", self.__modify) self.tree.register_callback("node-deleted", self.__delete) self.fbank = fbank self.name = name self.applied_filters = [] self.nodes = [] self.cllbcks = [] if refresh: self.__refresh() def __refresh(self): for node in self.tree.get_all_nodes(): self.__modify(node) self.initialized = True def apply_filter(self, filter_name, refresh=True): if self.fbank.has_filter(filter_name): if filter_name not in self.applied_filters: self.applied_filters.append(filter_name) if refresh: # If we are not initialized, we need to refresh with # all existing nodes if self.initialized: for n in list(self.nodes): self.__modify(n) else: self.__refresh() else: print("There's no filter called %s" % filter_name) def unapply_filter(self, filter_name): if filter_name in self.applied_filters: self.applied_filters.remove(filter_name) for node in self.tree.get_all_nodes(): self.__modify(node) # there's only one callback: "modified" def register_cllbck(self, func): if func not in self.cllbcks: self.cllbcks.append(func) def unregister_cllbck(self, func): if func in self.cllbacks: self.cllbacks.remove(func) def get_n_nodes(self): return len(self.nodes) def modify(self, nid): """ Allow external update of a given node """ self.__modify(nid) def __modify(self, nid): displayed = True for filtname in self.applied_filters: filt = self.fbank.get_filter(filtname) displayed &= filt.is_displayed(nid) if displayed: self.__add(nid) else: self.__delete(nid) def __delete(self, nid): if nid in self.nodes: self.nodes.remove(nid) self.__callback() def __add(self, nid): if nid not in self.nodes: self.nodes.append(nid) self.__callback() def __callback(self): for c in self.cllbcks: c() liblarch-3.2.0/liblarch/viewtree.py000066400000000000000000000264551420425305200172710ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- import functools from .filteredtree import FilteredTree # There should be two classes: for static and for dynamic mode # There are many conditions, and also we would prevent unallowed modes class ViewTree(object): def __init__(self, maininterface, maintree, filters_bank, name=None, refresh=True, static=False): """A ViewTree is the interface that should be used to display Tree(s). In static mode, FilteredTree layer is not created. (There is no need) We connect to MainTree or FilteredTree to get informed about changes. If FilteredTree is used, it is connected to MainTree to handle changes and then send id to ViewTree if it applies. @param maintree: a Tree object, cointaining all the nodes @param filters_bank: a FiltersBank object. Filters can be added dinamically to that. @param refresh: if True, this ViewTree is automatically refreshed after applying a filter. @param static: if True, this is the view of the complete maintree. Filters cannot be added to such a view. """ self.maininterface = maininterface self.__maintree = maintree self.__cllbcks = {} self.__fbank = filters_bank self.static = static if self.static: self._tree = self.__maintree self.__ft = None self.__maintree.register_callback( 'node-added', functools.partial(self.__emit, 'node-added')) self.__maintree.register_callback( 'node-deleted', functools.partial(self.__emit, 'node-deleted')) self.__maintree.register_callback( 'node-modified', functools.partial(self.__emit, 'node-modified')) else: self.__ft = FilteredTree( maintree, filters_bank, name=name, refresh=refresh) self._tree = self.__ft self.__ft.set_callback( 'added', functools.partial(self.__emit, 'node-added-inview')) self.__ft.set_callback( 'deleted', functools.partial(self.__emit, 'node-deleted-inview')) self.__ft.set_callback( 'modified', functools.partial(self.__emit, 'node-modified-inview')) self.__ft.set_callback( 'reordered', functools.partial(self.__emit, 'node-children-reordered')) def queue_action(self, node_id, func, param=None): self.__ft.set_callback('runonce', func, node_id=node_id, param=param) def get_basetree(self): """ Return Tree object """ return self.maininterface def register_cllbck(self, event, func): """ Store function and return unique key which can be used to unregister the callback later """ if event not in self.__cllbcks: self.__cllbcks[event] = {} callbacks = self.__cllbcks[event] key = 0 while key in callbacks: key += 1 callbacks[key] = func return key def deregister_cllbck(self, event, key): """ Remove the callback identifed by key (from register_cllbck) """ try: del self.__cllbcks[event][key] except KeyError: pass def __emit(self, event, node_id, path=None, neworder=None): """ Handle a new event from MainTree or FilteredTree by passing it to other objects, e.g. TreeWidget """ callbacks = dict(self.__cllbcks.get(event, {})) for func in callbacks.values(): if neworder: func(node_id, path, neworder) else: func(node_id, path) def get_node(self, node_id): """ Get a node from MainTree """ return self.__maintree.get_node(node_id) # FIXME Remove this method from public interface def get_root(self): return self.__maintree.get_root() # FIXME Remove this method from public interface def refresh_all(self): self.__maintree.refresh_all() def get_current_state(self): """ Request current state to be send by signals/callbacks. This allow LibLarch widget to connect on fly (e.g. after FilteredTree is up and has some nodes). """ if self.static: self.__maintree.refresh_all() else: self.__ft.get_current_state() def print_tree(self, string=None): """ Print the shown tree, i.e. MainTree or FilteredTree """ return self._tree.print_tree(string) def get_all_nodes(self): """ Return list of node_id of displayed nodes """ return self._tree.get_all_nodes() def get_n_nodes(self, withfilters=[]): """ Returns quantity of displayed nodes in this tree @withfilters => Additional filters are applied before counting, i.e. the currently applied filters are also taken into account """ if not self.__ft: self.__ft = FilteredTree( self.__maintree, self.__fbank, refresh=True) return self.__ft.get_n_nodes(withfilters=withfilters) def get_nodes(self, withfilters=[]): """ Returns displayed nodes in this tree @withfilters => Additional filters are applied before counting, i.e. the currently applied filters are also taken into account """ if not self.__ft: self.__ft = FilteredTree( self.__maintree, self.__fbank, refresh=True) return self.__ft.get_nodes(withfilters=withfilters) def get_node_for_path(self, path): """ Convert path to node_id. I am not sure what this is for... """ return self._tree.get_node_for_path(path) def get_paths_for_node(self, node_id=None): """ If node_id is none, return root path *Almost* reverse function to get_node_for_path (1 node can have many paths, 1:M) """ return self._tree.get_paths_for_node(node_id) # FIXME change pid => parent_id def next_node(self, node_id, pid=None): """ Return the next node to node_id. @parent_id => identify which instance of node_id to work. If None, random instance is used """ return self._tree.next_node(node_id, pid) def node_has_child(self, node_id): """ Has the node at least one child? """ if self.static: return self.__maintree.get_node(node_id).has_child() else: return self.__ft.node_has_child(node_id) def node_all_children(self, node_id=None): """ Return children of a node """ if self.static: if not node_id or self.__maintree.has_node(node_id): return self.__maintree.get_node(node_id).get_children() else: return [] else: return self._tree.node_all_children(node_id) def node_n_children(self, node_id=None, recursive=False): """ Return quantity of children of node_id. If node_id is None, use the root node. Every instance of node has the same children""" if not self.__ft: self.__ft = FilteredTree( self.__maintree, self.__fbank, refresh=True) return self.__ft.node_n_children(node_id, recursive) def node_nth_child(self, node_id, n): """ Return nth child of the node. """ if self.static: if not node_id or node_id == 'root': node = self.__maintree.get_root() else: node = self.__maintree.get_node(node_id) if node and node.get_n_children() > n: return node.get_nth_child(n) else: raise ValueError( "node {} has less than {} nodes".format(node_id, n)) else: realn = self.__ft.node_n_children(node_id) if realn <= n: raise ValueError( "viewtree has {} nodes, no node {}".format(realn, n)) return self.__ft.node_nth_child(node_id, n) def node_has_parent(self, node_id): """ Has node parents? Is it child of root? """ return len(self.node_parents(node_id)) > 0 def node_parents(self, node_id): """ Returns displayed parents of the given node, or [] if there is no parent (such as if the node is a child of the virtual root), or if the parent is not displayable. Doesn't check wheter node node_id is displayed or not. (we only care about parents) """ if self.static: return self.__maintree.get_node(node_id).get_parents() else: return self.__ft.node_parents(node_id) def is_displayed(self, node_id): """ Is the node displayed? """ if self.static: return self.__maintree.has_node(node_id) else: return self.__ft.is_displayed(node_id) # FILTERS ################################################################# def list_applied_filters(self): return self.__ft.list_applied_filters() def apply_filter(self, filter_name, parameters=None, reset=False, refresh=True): """ Applies a new filter to the tree. @param filter_name: The name of an already registered filter to apply @param parameters: Optional parameters to pass to the filter @param reset: optional boolean. Should we remove other filters? @param refresh : should we refresh after applying this filter ? """ if self.static: raise Exception("WARNING: filters cannot be applied" "to a static tree\n") self.__ft.apply_filter(filter_name, parameters, reset, refresh) def unapply_filter(self, filter_name, refresh=True): """ Removes a filter from the tree. @param filter_name: The name of filter to remove """ if self.static: raise Exception("WARNING: filters cannot be unapplied" "from a static tree\n") self.__ft.unapply_filter(filter_name, refresh) def reset_filters(self, refresh=True): """ Remove all filters currently set on the tree. """ if self.static: raise Exception("WARNING: filters cannot be reset" "on a static tree\n") else: self.__ft.reset_filters(refresh) liblarch-3.2.0/liblarch_gtk/000077500000000000000000000000001420425305200157165ustar00rootroot00000000000000liblarch-3.2.0/liblarch_gtk/__init__.py000066400000000000000000000552551420425305200200430ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- import gi # noqa gi.require_version("Gtk", "3.0") # noqa from gi.repository import Gtk, Gdk from gi.repository import GObject from .treemodel import TreeModel # Useful for debugging purpose. # Disabling that will disable the TreeModelSort on top of our TreeModel ENABLE_SORTING = True USE_TREEMODELFILTER = False BRITGHTNESS_CACHE = {} def brightness(color_str): """ Compute brightness of a color on scale 0-1 Courtesy to http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color """ global BRITGHTNESS_CACHE if color_str not in BRITGHTNESS_CACHE: c = Gdk.color_parse(color_str) brightness = (0.2126 * c.red + 0.7152 * c.green + 0.0722 * c.blue) / 65535.0 BRITGHTNESS_CACHE[color_str] = brightness return BRITGHTNESS_CACHE[color_str] class TreeView(Gtk.TreeView): """ Widget which display LibLarch FilteredTree. This widget extends Gtk.TreeView by several features: * Drag'n'Drop support * Sorting support * separator rows * background color of a row * selection of multiple rows """ __string_signal__ = (GObject.SignalFlags.RUN_FIRST, None, (str, )) __gsignals__ = { 'node-expanded': __string_signal__, 'node-collapsed': __string_signal__, } def __init__(self, tree, description): """ Build the widget @param tree - LibLarch FilteredTree @param description - definition of columns. Parameters of description dictionary for a column: * value => (type of values, function for generating value from a node) * renderer => (renderer_attribute, renderer object) Optional: * order => specify order of column otherwise use natural order * expandable => is the column expandable? * resizable => is the column resizable? * visible => is the column visible? * title => title of column * new_colum => do not create a separate column, just continue with the previous one (this can be used to create columns without borders) * sorting => allow default sorting on this column * sorting_func => use special function for sorting on this func Example of columns descriptions: description = { 'title': { 'value': [str, self.task_title_column], 'renderer': ['markup', Gtk.CellRendererText()], 'order': 0 }} """ GObject.GObject.__init__(self) self.columns = {} self.sort_col = None self.sort_order = Gtk.SortType.ASCENDING self.bg_color_column = None self.separator_func = None self.dnd_internal_target = '' self.dnd_external_targets = {} # Sort columns self.order_of_column = [] last = 9999 for col_name in description: desc = description[col_name] order = desc.get('order', last) last += 1 self.order_of_column.append((order, col_name)) types = [] sorting_func = [] # Build the first coulumn if user starts with new_colum=False col = Gtk.TreeViewColumn() # Build columns according to the order for col_num, (order_num, col_name) in enumerate( sorted(self.order_of_column), 1): desc = description[col_name] types.append(desc['value']) expand = desc.get('expandable', False) resizable = desc.get('resizable', True) visible = desc.get('visible', True) if 'renderer' in desc: rend_attribute, renderer = desc['renderer'] else: rend_attribute = 'markup' renderer = Gtk.CellRendererText() # If new_colum=False, do not create new column, use the previous # one. It will create columns without borders. if desc.get('new_column', True): col = Gtk.TreeViewColumn() newcol = True else: newcol = False col.set_visible(visible) if 'title' in desc: col.set_title(desc['title']) col.pack_start(renderer, expand=expand) col.add_attribute(renderer, rend_attribute, col_num) col.set_resizable(resizable) col.set_expand(expand) # Allow to set background color col.set_cell_data_func(renderer, self._celldatafunction) if newcol: self.append_column(col) self.columns[col_name] = (col_num, col) if ENABLE_SORTING: if 'sorting' in desc: # Just allow sorting and use default comparing self.sort_col = desc['sorting'] sort_num, sort_col = self.columns[self.sort_col] col.set_sort_column_id(sort_num) if 'sorting_func' in desc: # Use special funcion for comparing, e.g. dates sorting_func.append((col_num, col, desc['sorting_func'])) self.basetree = tree # Build the model around LibLarch tree self.basetreemodel = TreeModel(tree, types) # Applying an intermediate treemodelfilter, for debugging purpose if USE_TREEMODELFILTER: treemodelfilter = self.basetreemodel.filter_new() else: treemodelfilter = self.basetreemodel # Apply TreeModelSort to be able to sort if ENABLE_SORTING: self.treemodel = self.basetreemodel for col_num, col, sort_func in sorting_func: self.treemodel.set_sort_func( col_num, self._sort_func, sort_func) col.set_sort_column_id(col_num) else: self.treemodel = treemodelfilter self.set_model(self.treemodel) self.expand_all() self.show() self.collapsed_paths = [] self.connect('row-expanded', self.__emit, 'expanded') self.connect('row-collapsed', self.__emit, 'collapsed') self.treemodel.connect('row-has-child-toggled', self.on_child_toggled) def __emit(self, sender, iter, path, data): """ Emit expanded/collapsed signal """ # recreating the path of the collapsed node ll_path = () i = 1 path = path.get_indices() while i <= len(path): temp_path = Gtk.TreePath(":".join(str(n) for n in path[:i])) temp_iter = self.treemodel.get_iter(temp_path) ll_path += (self.treemodel.get_value(temp_iter, 0), ) i += 1 if data == 'expanded': self.emit('node-expanded', ll_path) elif data == 'collapsed': self.emit('node-collapsed', ll_path) def on_child_toggled(self, treemodel, path, iter, param=None): """ Expand row """ # is the toggled node in the collapsed paths? collapsed = False nid = treemodel.get_value(iter, 0) while iter and not collapsed: for c in self.collapsed_paths: if c[-1] == nid: collapsed = True iter = treemodel.iter_parent(iter) if not self.row_expanded(path) and not collapsed: self.expand_row(path, True) def expand_node(self, llpath): """ Expand the children of a node. This is not recursive """ self.collapse_node(llpath, collapsing_method=self.expand_one_row) def expand_one_row(self, p): # We have to set the "open all" parameters self.expand_row(p, False) def collapse_node(self, llpath, collapsing_method=None): """ Hide children of a node This method is needed for "rember collapsed nodes" feature of GTG. Transform node_id into paths and those paths collapse. By default all children are expanded (see self.expand_all()) @parameter llpath - LibLarch path to the node. Node_id is extracted as the last parameter and then all instances of that node are collapsed. For retro-compatibility, we take llpath instead of node_id directly""" if not collapsing_method: collapsing_method = self.collapse_row node_id = llpath[-1].strip("'") if not node_id: raise Exception('Missing node_id in path %s' % str(llpath)) schedule_next = True for path in self.basetree.get_paths_for_node(node_id): iter = self.basetreemodel.my_get_iter(path) if iter is None: continue target_path = self.basetreemodel.get_path(iter) if self.basetreemodel.get_value(iter, 0) == node_id: collapsing_method(target_path) self.collapsed_paths.append(path) schedule_next = False if schedule_next: self.basetree.queue_action( node_id, self._collapse_node_retry, param=(llpath, collapsing_method)) def _collapse_node_retry(self, param): """ Node to be collapsed/expand found, so re-try now with correct and preserved parameters. """ self.collapse_node(param[0], collapsing_method=param[1]) def show(self): """ Shows the TreeView and connect basetreemodel to LibLarch """ Gtk.TreeView.show(self) self.basetreemodel.connect_model() def get_columns(self): """ Return the list of columns name """ return list(self.columns.keys()) def set_main_search_column(self, col_name): """ Set search column for GTK integrate search This is just wrapper to use internal representation of columns""" col_num, col = self.columns[col_name] self.set_search_column(col_num) def set_expander_column(self, col_name): """ Set expander column (that which expands through free space) This is just wrapper to use internal representation of columns""" col_num, col = self.columns[col_name] self.set_property("expander-column", col) def set_sort_column(self, col_name, order=Gtk.SortType.ASCENDING): """ Select column to sort by it by default """ if ENABLE_SORTING: self.sort_col = col_name self.sort_order = order col_num, col = self.columns[col_name] self.treemodel.set_sort_column_id(col_num, order) def get_sort_column(self): """ Get sort column """ if ENABLE_SORTING: return self.sort_col, self.sort_order def set_col_visible(self, col_name, visible): """ Set visiblity of column. Allow to hide/show certain column """ col_num, col = self.columns[col_name] col.set_visible(visible) def set_col_resizable(self, col_name, resizable): """ Allow/forbid column to be resizable """ col_num, col = self.columns[col_name] col.set_resizable(resizable) def set_bg_color(self, color_func, color_column): """ Set which column and function for generating background color Function should be in format func(node, default_color) """ def closure_default_color(func, column): """ Set default color to the function. Transform function from func(node, default_color) into func(node). Default color is computed based on some GTK style magic. """ style = column.get_tree_view().get_style_context() color = style.get_background_color(Gtk.StateFlags.NORMAL) default = color.to_color() return lambda node: func(node, default) if color_column in self.columns: self.bg_color_column, column = self.columns[color_column] func = closure_default_color(color_func, column) self.treemodel.set_column_function(self.bg_color_column, func) else: raise ValueError( "There is no colum %s to use to set color" % color_column) def _sort_func(self, model, iter1, iter2, func=None): """ Sort two iterators by function which gets node objects. This is a simple wrapper which prepares node objects and then call comparing function. In other case return default value -1 """ node_id_a = model.get_value(iter1, 0) node_id_b = model.get_value(iter2, 0) if node_id_a and node_id_b and func: id, order = self.treemodel.get_sort_column_id() node_a = self.basetree.get_node(node_id_a) node_b = self.basetree.get_node(node_id_b) sort = func(node_a, node_b, order) else: sort = -1 return sort def _celldatafunction(self, column, cell, model, myiter, user_data): """ Determine background color for cell Requirements: self.bg_color_column must be set (see self.set_bg_color()) Set background color based on a certain column value. """ if self.bg_color_column is None: return if myiter and model.iter_is_valid(myiter): color = model.get_value(myiter, self.bg_color_column) else: color = None if isinstance(cell, Gtk.CellRendererText): if color is not None and brightness(color) < 0.5: cell.set_property("foreground", '#FFFFFF') else: # Otherwise unset foreground color cell.set_property("foreground-set", False) cell.set_property("cell-background", color) # DRAG-N-DROP functions ##################################### def set_dnd_name(self, dndname): """ Sets Drag'n'Drop name and initialize Drag'n'Drop support If ENABLE_SORTING, drag_drop signal must be handled by this widget.""" self.dnd_internal_target = dndname self.__init_dnd() self.connect('drag_data_get', self.on_drag_data_get) self.connect('drag_data_received', self.on_drag_data_received) def set_dnd_external(self, sourcename, func): """ Add a new external target and initialize Drag'n'Drop support""" i = 1 while i in self.dnd_external_targets: i += 1 self.dnd_external_targets[i] = [sourcename, func] self.__init_dnd() def __init_dnd(self): """ Initialize Drag'n'Drop support Firstly build list of DND targets: * name * scope - just the same widget / same application * id Enable DND by calling enable_model_drag_dest(), enable_model-drag_source() It didnt use support from Gtk.Widget(drag_source_set(), drag_dest_set()). To know difference, look in PyGTK FAQ: http://faq.pyGtk.org/index.py?file=faq13.033.htp&req=show """ self.defer_select = False if self.dnd_internal_target == '': error = 'Cannot initialize DND without a valid name\n' error += 'Use set_dnd_name() first' raise Exception(error) dnd_targets = [( self.dnd_internal_target, Gtk.TargetFlags.SAME_WIDGET, 0)] for target in self.dnd_external_targets: name = self.dnd_external_targets[target][0] dnd_targets.append((name, Gtk.TargetFlags.SAME_APP, target)) self.enable_model_drag_source( Gdk.ModifierType.BUTTON1_MASK, dnd_targets, Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE) self.enable_model_drag_dest( dnd_targets, Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE) def on_drag_data_get(self, treeview, context, selection, info, timestamp): """ Extract data from the source of the DnD operation. Serialize iterators of selected tasks in format , , ..., and set it as parameter of DND """ treeselection = treeview.get_selection() model, paths = treeselection.get_selected_rows() iters = [model.get_iter(path) for path in paths] iter_str = ','.join(model.get_string_from_iter(iter) for iter in iters) selection.set(selection.get_target(), 0, iter_str.encode('ascii')) def on_drag_data_received(self, treeview, context, x, y, selection, info, timestamp): """ Handle a drop situation. First of all, we need to get id of node which should accept all draged nodes as their new children. If there is no node, drop to root node. Deserialize iterators of dragged nodes (see self.on_drag_data_get()) Info parameter determines which target was used: * info == 0 => internal DND within this TreeView * info > 0 => external DND In case of internal DND we just use Tree.move_node(). In case of external DND we call function associated with that DND set by self.set_dnd_external() """ # TODO: it should be configurable for each TreeView if you want: # 0 : no drag-n-drop at all # 1 : drag-n-drop move the node # 2 : drag-n-drop copy the node model = treeview.get_model() drop_info = treeview.get_dest_row_at_pos(x, y) if drop_info: path, position = drop_info iter = model.get_iter(path) # Must add the task to the parent of the task situated # before/after if position == Gtk.TreeViewDropPosition.BEFORE or\ position == Gtk.TreeViewDropPosition.AFTER: # Get sibling parent destination_iter = model.iter_parent(iter) else: # Must add task as a child of the dropped-on iter # Get parent destination_iter = iter if destination_iter: destination_tid = model.get_value(destination_iter, 0) else: # it means we have drag-n-dropped above the first task # we should consider the destination as a root then. destination_tid = None else: # Must add the task to the root # Parent = root => iter=None destination_tid = None tree = self.basetree.get_basetree() # Get dragged iter as a TaskTreeModel iter # If there is no selected task (empty selection.data), # explictly skip handling it (set to empty list) data = selection.get_data() if data == '': iters = [] else: iters = data.decode().split(',') dragged_iters = [] for iter in iters: if info == 0: try: dragged_iters.append(model.get_iter_from_string(iter)) except ValueError: # I hate to silently fail but we have no choice. # It means that the iter is not good. # Thanks shitty gtk API for not allowing us to test # the string dragged_iter = None # Handle drag from one widget to another (ex: treeview to treeview) elif info in self.dnd_external_targets and destination_tid: f = self.dnd_external_targets[info][1] src_model = Gtk.drag_get_source_widget(context).get_model() dragged_iters.append(src_model.get_iter_from_string(iter)) for dragged_iter in dragged_iters: if info == 0: if dragged_iter and model.iter_is_valid(dragged_iter): dragged_tid = model.get_value(dragged_iter, 0) try: tree.move_node( dragged_tid, new_parent_id=destination_tid) except Exception as e: print('Problem with dragging: %s' % e) # Handle inter-widget Drag'n'Drop again (like in the previous loop) elif info in self.dnd_external_targets and destination_tid: source = src_model.get_value(dragged_iter, 0) f(source, destination_tid) # Separators support ############################################## def _separator_func(self, model, itera, user_data=None): """ Call user function to determine if this node is separator """ if itera and model.iter_is_valid(itera): node_id = model.get_value(itera, 0) node = self.basetree.get_node(node_id) if self.separator_func: return self.separator_func(node) else: return False else: return False def set_row_separator_func(self, func, data=None): """ Enable support for row separators. @param func - function which determines if a node is separator, None will disable support for row separators. """ self.separator_func = func Gtk.TreeView.set_row_separator_func(self, self._separator_func, data) # Multiple selection #################################################### def get_selected_nodes(self): """ Return list of node_id from liblarch for selected nodes """ # Get the selection in the Gtk.TreeView selection = self.get_selection() # Get the selection iter if selection.count_selected_rows() <= 0: ids = [] else: model, paths = selection.get_selected_rows() iters = [model.get_iter(path) for path in paths] ts = self.get_model() # 0 is the column of the tid ids = [ts.get_value(iter, 0) for iter in iters] return ids def set_multiple_selection(self, multiple_selection): """ Allow/forbid multiple selection in TreeView """ # TODO support for dragging multiple rows at the same time # See LP #817433 if multiple_selection: selection_type = Gtk.SelectionMode.MULTIPLE else: selection_type = Gtk.SelectionMode.SINGLE self.get_selection().set_mode(selection_type) liblarch-3.2.0/liblarch_gtk/treemodel.py000066400000000000000000000170121420425305200202510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- from gi.repository import Gtk class TreeModel(Gtk.TreeStore): """ Local copy of showed tree """ def __init__(self, tree, types): """ Initializes parent and create list of columns. The first colum is node_id of node """ self.count = 0 self.count2 = 0 self.types = [[str, lambda node: node.get_id()]] + types only_types = [python_type for python_type, access_method in self.types] super(TreeModel, self).__init__(*only_types) self.cache_paths = {} self.cache_position = {} self.tree = tree def set_column_function(self, column_num, column_func): """ Replace function for generating certain column. Original use case was changing method of generating background color during runtime - background by tags or due date """ if column_num < len(self.types): self.types[column_num][1] = column_func return True else: return False def connect_model(self): """ Register "signals", callbacks from liblarch. Also asks for the current status by providing add_task callback. We are able to connect to liblarch tree on the fly. """ self.tree.register_cllbck('node-added-inview', self.add_task) self.tree.register_cllbck('node-deleted-inview', self.remove_task) self.tree.register_cllbck('node-modified-inview', self.update_task) self.tree.register_cllbck('node-children-reordered', self.reorder_nodes) # Request the current state self.tree.get_current_state() def my_get_iter(self, path): """ Because we sort the TreeStore, paths in the treestore are not the same as paths in the FilteredTree. We do the conversion here. We receive a Liblarch path as argument and return a Gtk.TreeIter""" # The function is recursive. We take iter for path (A, B, C) in cache. # If there is not, we take iter for path (A, B) and try to find C. if path == (): return None nid = str(path[-1]) self.count += 1 # We try to use the cache iter = self.cache_paths.get(path, None) toreturn = None if (iter and self.iter_is_valid(iter) and nid == self.get_value(iter, 0)): self.count2 += 1 toreturn = iter else: root = self.my_get_iter(path[:-1]) # This is a small ad-hoc optimisation. # Instead of going through all the children nodes # We go directly at the last known position. pos = self.cache_position.get(path, None) if pos: iter = self.iter_nth_child(root, pos) if iter and self.get_value(iter, 0) == nid: toreturn = iter if not toreturn: if root: iter = self.iter_children(root) else: iter = self.get_iter_first() while iter and self.get_value(iter, 0) != nid: iter = self.iter_next(iter) self.cache_paths[path] = iter toreturn = iter return toreturn def print_tree(self): """ Print TreeStore as Tree into console """ def push_to_stack(stack, level, iterator): """ Macro which adds a new element into stack if it is possible """ if iterator is not None: stack.append((level, iterator)) stack = [] push_to_stack(stack, 0, self.get_iter_first()) print("+" * 50) print("Treemodel print_tree: ") while stack != []: level, iterator = stack.pop() print("=>" * level, self.get_value(iterator, 0)) push_to_stack(stack, level, self.iter_next(iterator)) push_to_stack(stack, level + 1, self.iter_children(iterator)) print("+" * 50) # INTERFACE TO LIBLARCH ################################################### def add_task(self, node_id, path): """ Add new instance of node_id to position described at path. @param node_id: identification of task @param path: identification of position """ node = self.tree.get_node(node_id) # Build a new row row = [] for python_type, access_method in self.types: value = access_method(node) row.append(value) # Find position to add task iter_path = path[:-1] iterator = self.my_get_iter(iter_path) self.cache_position[path] = self.iter_n_children(iterator) self.insert(iterator, -1, row) def remove_task(self, node_id, path): """ Remove instance of node. @param node_id: identification of task @param path: identification of position """ it = self.my_get_iter(path) if not it: raise Exception( "Trying to remove node %s with no iterator" % node_id) actual_node_id = self.get_value(it, 0) assert actual_node_id == node_id self.remove(it) self.cache_position.pop(path) def update_task(self, node_id, path): """ Update instance of node by rebuilding the row. @param node_id: identification of task @param path: identification of position """ # We cannot assume that the node is in the tree because # update is asynchronus # Also, we should consider that missing an update is not critical # and ignoring the case where there is no iterator if self.tree.is_displayed(node_id): node = self.tree.get_node(node_id) # That call to my_get_iter is really slow! iterator = self.my_get_iter(path) if iterator: for column_num, (__, access_method) in enumerate(self.types): value = access_method(node) if value is not None: self.set_value(iterator, column_num, value) def reorder_nodes(self, node_id, path, neworder): """ Reorder nodes. This is deprecated signal. In the past it was useful for reordering showed nodes of tree. It was possible to delete just the last element and therefore every element must be moved to the last position and then deleted. @param node_id: identification of root node @param path: identification of poistion of root node @param neworder: new order of children of root node """ if path is not None: it = self.my_get_iter(path) else: it = None self.reorder(it, neworder) self.print_tree() liblarch-3.2.0/main.py000077500000000000000000000455031420425305200146010ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- from random import randint, choice, shuffle from time import sleep, time import logging import os import re import sys import threading from liblarch import Tree from liblarch import TreeNode from liblarch_gtk import TreeView from gi.repository import Gtk from gi.repository import GObject # Set up logging logging.basicConfig(level=logging.INFO) # Constants LOAD_MANY_TASKS_COUNT = 1000 ADD_MANY_TASKS_TO_EXISTING_TASKS = True SLEEP_BETWEEN_TASKS = 0 # Useful for experimenting with the tree BACKUP_OPERATIONS = False def random_task_title_on_id(t_id): try: return 'Task %5d' % int(t_id) except ValueError: return 'Task %5s' % t_id # Generate title in different ways random_task_title = random_task_title_on_id MAX_FILE_ID = 0 def save_backup(function): def _save_backup(*args, **kwargs): global MAX_FILE_ID self = args[0] file_name = "operation_%03d.bak" % MAX_FILE_ID while os.path.exists(file_name): MAX_FILE_ID += 1 file_name = "operation_%03d.bak" % MAX_FILE_ID stdout = sys.stdout stderr = sys.stderr output = open(file_name, "w", 0) sys.stdout = output sys.stderr = output print("Tree before operation") self.print_tree() print("\nOperation '%s':" % (function.__name__)) res = function(*args, **kwargs) print("Tree after operation") self.print_tree() sys.stdout = stdout sys.stderr = stderr output.close() # Print the log output = open(file_name, "r") print(output.read()) output.close() return res if BACKUP_OPERATIONS: return _save_backup else: return function MAX_ID = 0 def random_id(): global MAX_ID MAX_ID += 1 return str(MAX_ID) class TaskNode(TreeNode): def __init__(self, tid, label, viewtree): TreeNode.__init__(self, tid) self.label = label self.tid = tid self.vt = viewtree def get_label(self): return "%s (%s children)" % ( self.label, self.vt.node_n_children(self.tid, recursive=True)) class Backend(threading.Thread): def __init__(self, backend_id, finish_event, delay, tree, viewtree): super().__init__() self.backend_id = backend_id self.delay = delay self.tree = tree self.viewtree = viewtree self.finish_event = finish_event def run(self): counter = 0 parent_id = None while not self.finish_event.wait(self.delay): task_id = self.backend_id + "_" + str(counter) title = task_id node = TaskNode(task_id, title, self.viewtree) self.tree.add_node(node, parent_id, self.tree) parent_id = task_id # Delete some tasks for i in range(randint(3, 10)): delete_id = "{}sec_{}".format( choice([1, 3, 5]), randint(0, 2 * counter)) logging.info("%s deleting %s", self.backend_id, delete_id) self.tree.del_node(delete_id) counter += 1 logging.info("%s --- finish", self.backend_id) class LiblarchDemo(object): """ Shows a simple GUI demo of liblarch usage with several functions for adding tasks """ def _build_tree_view(self): self.tree = Tree() self.tree.add_filter("even", self.even_filter) self.tree.add_filter("odd", self.odd_filter) self.tree.add_filter("flat", self.flat_filter, {"flat": True}) self.tree.add_filter("leaf", self.leaf_filter) self.view_tree = self.tree.get_viewtree() self.mod_counter = 0 self.view_tree.register_cllbck( 'node-added-inview', self._update_title) self.view_tree.register_cllbck( 'node-modified-inview', self._modified_count) self.view_tree.register_cllbck( 'node-deleted-inview', self._update_title) desc = {} col_name = 'label' col = {} col['title'] = "Title" col['value'] = [str, self.task_label_column] col['expandable'] = True col['resizable'] = True col['sorting'] = 'label' col['order'] = 0 desc[col_name] = col tree_view = TreeView(self.view_tree, desc) # Polish TreeView def on_row_activate(sender, a, b): logging.info( "Selected nodes are: %s", str(tree_view.get_selected_nodes())) tree_view.set_dnd_name('liblarch-demo/liblarch_widget') tree_view.set_multiple_selection(True) tree_view.set_property("enable-tree-lines", True) tree_view.connect('row-activated', on_row_activate) return tree_view def even_filter(self, node): if node.get_id().isdigit(): return int(node.get_id()) % 2 == 0 else: return False def odd_filter(self, node): return not self.even_filter(node) def flat_filter(self, node, parameters=None): return True def leaf_filter(self, node): return not node.has_child() def _modified_count(self, nid, path): logging.debug("Node %s has been modified", nid) self.mod_counter += 1 def _update_title(self, sender, nid): count = self.view_tree.get_n_nodes() if count == LOAD_MANY_TASKS_COUNT and self.start_time > 0: stop_time = time() - self.start_time logging.info( "Time to load %s tasks: %s", LOAD_MANY_TASKS_COUNT, stop_time) mean = self.mod_counter * 1.0 / count logging.info( "%s modified signals were received (%s per task)", self.mod_counter, mean) self.window.set_title('Liblarch demo: %s nodes' % count) def __init__(self): self.window = Gtk.Window() self.window.set_size_request(640, 480) self.window.set_position(Gtk.WindowPosition.CENTER) self.window.set_border_width(10) self.window.set_title('Liblarch demo') self.window.connect('destroy', self.finish) self.liblarch_widget = self._build_tree_view() scrolled_window = Gtk.ScrolledWindow() scrolled_window.add_with_viewport(self.liblarch_widget) self.start_time = 0 # Buttons action_panel = Gtk.Box() action_panel.set_spacing(5) button_desc = [ ('_Add a Task', self.add_task), ('_Delete a Task', self.delete_task), ('_Print Tree', self.print_tree), ('_Print FT', self.print_ft), ('_Load many Tasks', self.many_tasks), ('_Quit', self.finish), ] for name, callback in button_desc: button = Gtk.Button.new_with_mnemonic(name) button.connect('clicked', callback) action_panel.pack_start(button, True, True, 0) filter_panel = Gtk.Box() filter_panel.set_spacing(5) for name in self.tree.list_filters(): button = Gtk.ToggleButton("%s filter" % name) button.connect('toggled', self.apply_filter, name) filter_panel.pack_start(button, True, True, 0) # Use cases usecases_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) usecase_box = None usecase_order = 0 usecase_order_max = 3 button_desc = [ ('_Tree high 3', self.tree_high_3), ('Tree high 3 backwards', self.tree_high_3_backwards), ('Load from file', self.load_from_file), ('Delete DFXBCAE', self.delete_magic), ('Delete backwards', self.delete_backwards), ('Delete randomly', self.delete_random), ('Change task', self.change_task), ('_Backend use case', self.backends), ] for name, callback in button_desc: if usecase_order <= 0: if usecase_box is not None: usecases_vbox.pack_start( usecase_box, expand=False, fill=True, padding=0) usecase_box = Gtk.Box() usecase_box.set_spacing(5) button = Gtk.Button.new_with_mnemonic(name) button.connect('clicked', callback) usecase_box.pack_start(button, True, True, 0) usecase_order = (usecase_order + 1) % usecase_order_max usecases_vbox.pack_start( usecase_box, expand=False, fill=True, padding=0) usecase_panel = Gtk.Expander() usecase_panel.set_label('Use cases') usecase_panel.set_expanded(True) usecase_panel.add(usecases_vbox) # Show it vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) vbox.pack_start(action_panel, False, True, 10) vbox.pack_start(filter_panel, False, True, 10) vbox.pack_start(scrolled_window, True, True, 0) vbox.pack_start(usecase_panel, False, True, 10) self.window.add(vbox) self.window.show_all() self.should_finish = threading.Event() def task_label_column(self, node): newlabel = node.get_label() return newlabel def print_tree(self, widget=None): print() print("=" * 20, "Tree", "=" * 20) self.tree.get_main_view().print_tree() print("=" * 46) print() def print_ft(self, widget=None): print() self.view_tree.print_tree() print() @save_backup def add_task(self, widget): """ Add a new task. If a task is selected, the new task is added as its child """ selected = self.liblarch_widget.get_selected_nodes() t_id = random_id() t_title = random_task_title(t_id) task = TaskNode(t_id, t_title, self.view_tree) if len(selected) == 1: # Adding a subchild parent = selected[0] self.tree.add_node(task, parent_id=parent) logging.info( 'Added sub-task "%s" (%s) for %s', t_title, t_id, parent) else: # Adding as a new child self.tree.add_node(task) for parent_id in selected: task.add_parent(parent_id) logging.info('Added task "%s" (%s)', t_title, t_id) def apply_filter(self, widget, param): logging.info("applying filter: %s", param) if param in self.view_tree.list_applied_filters(): self.view_tree.unapply_filter(param) else: self.view_tree.apply_filter(param) @save_backup def tree_high_3(self, widget): ''' We add the leaf nodes before the root, in order to test if it works fine even in this configuration''' logging.info('Adding a tree of height 3') selected = self.liblarch_widget.get_selected_nodes() if len(selected) == 1: parent = selected[0] else: parent = None t_id = random_id() t_title = random_task_title(t_id) roottask = TaskNode(t_id, t_title, self.view_tree) local_parent = t_id for i in range(2): t_id = random_id() t_title = random_task_title(t_id) task = TaskNode(t_id, t_title, self.view_tree) self.tree.add_node(task, parent_id=local_parent) # Task becomes a parent for new task local_parent = t_id self.tree.add_node(roottask, parent_id=parent) @save_backup def tree_high_3_backwards(self, widget): logging.info('Adding a tree of height 3 backwards') selected = self.liblarch_widget.get_selected_nodes() if len(selected) == 1: parent = selected[0] else: parent = None tasks = [] relationships = [] for i in range(3): t_id = random_id() t_title = random_task_title(t_id) task = TaskNode(t_id, t_title, self.view_tree) tasks.append((t_id, task)) if parent is not None: relationships.append((parent, t_id)) parent = t_id # Relationships can come in any order, e.g. reversed relationships = reversed(relationships) for t_id, task in tasks: logging.info("Adding task to tree: %s %s", t_id, task) self.tree.add_node(task) logging.info("=" * 50) for parent, child in relationships: logging.info("New relationship: %s with %s", parent, child) parent_node = self.tree.get_node(parent) parent_node.add_child(child) logging.info("=" * 50) @save_backup def delete_task(self, widget, order='normal'): logging.info('Deleting a task, order: %s', order) selected = self.liblarch_widget.get_selected_nodes() if order == 'normal': ordered_nodes = selected elif order == 'backward': ordered_nodes = reversed(selected) elif order == 'random': ordered_nodes = selected shuffle(ordered_nodes) # Replace iterator for a list => we want to see the order in logs # and the performance is not important ordered_nodes = [node for node in ordered_nodes] elif order == 'magic-combination': # testing a special case from examples/test_suite ordered_nodes = ['D', 'F', 'X', 'B', 'C', 'A', 'E'] else: logging.error('Unknown order, skipping...') return logging.info( "Tasks should be removed in this order: %s", ordered_nodes) for node_id in ordered_nodes: self.tree.del_node(node_id) logging.info('Removed node %s', node_id) self.print_tree(None) def delete_backwards(self, widget): """ Delete task backward """ self.delete_task(widget, order='backward') def delete_random(self, widget): """ Delete tasks in random order """ self.delete_task(widget, order='random') def delete_magic(self, widget): self.delete_task(widget, order='magic-combination') def change_task(self, widget): for node_id in self.liblarch_widget.get_selected_nodes(): node = self.tree.get_node(node_id) node.label = "Hello" node.modified() def backends(self, widget): logging.info("Backends....") Backend( '1sec', self.should_finish, 1, self.tree, self.view_tree).start() Backend( '3sec', self.should_finish, 3, self.tree, self.view_tree).start() Backend( '5sec', self.should_finish, 5, self.tree, self.view_tree).start() widget.set_sensitive(False) def many_tasks(self, widget): self.start_time = time() def _many_tasks(): tasks_ids = [] prefix = randint(1, 1000) * 100000 for i in range(LOAD_MANY_TASKS_COUNT): t_id = str(prefix + i) t_title = t_id task = TaskNode(t_id, t_title, self.view_tree) # There is 25 % chance to adding as a sub_task if tasks_ids != [] and randint(0, 100) < 90: parent = choice(tasks_ids) self.tree.add_node(task, parent_id=parent) else: self.tree.add_node(task) tasks_ids.append(t_id) # Sleep 0.01 second to create illusion of real tasks sleep(SLEEP_BETWEEN_TASKS) logging.info("end of _many_tasks thread") t = threading.Thread(target=_many_tasks) t.start() def load_from_file(self, widget): dialog = Gtk.FileChooserDialog( "Open..", self.window, Gtk.FileChooserAction.OPEN, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) dialog.set_default_response(Gtk.ResponseType.OK) response = dialog.run() if response == Gtk.ResponseType.OK: file_name = dialog.get_filename() else: file_name = None dialog.destroy() if file_name is None: return log = open(file_name, 'r').read() m = re.match( r'\s*Tree before operation\s+=+\s+Tree\s+=+\s+(.*?)=+', log, re.UNICODE | re.DOTALL) if m: treelines = m.group(1) items = [(len(line) - len(line.lstrip()), line.strip()) for line in treelines.splitlines()] # Filter "root" item and decrease level items = [(level, name) for level, name in items[1:]] # The "root" items should be at level 0, adjust level to that min_level = min(level for level, name in items) items = [(level - min_level, name) for level, name in items] nodes = list(set([name for level, name in items])) relationships = [] parent_level = {-1: None} for level, name in items: parent = parent_level[level - 1] relationships.append((parent, name)) for key in list(parent_level.keys()): if key > level: del parent_level[key] parent_level[level] = name logging.info("Nodes to add:", nodes) logging.info("Relationships:", "\n".join(str(r) for r in relationships)) for node_id in nodes: task = TaskNode( node_id, random_task_title(node_id), self.view_tree) self.tree.add_node(task) for parent, child in relationships: parent_node = self.tree.get_node(parent) parent_node.add_child(child) else: logging.info("Not matched") logging.info("Log: %s", log) def finish(self, widget): self.should_finish.set() Gtk.main_quit() def run(self): Gtk.main() if __name__ == "__main__": GObject.threads_init() app = LiblarchDemo() app.run() # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 liblarch-3.2.0/requirements.txt000066400000000000000000000000371420425305200165550ustar00rootroot00000000000000nose spec pyflakes pycodestyle liblarch-3.2.0/run-tests000077500000000000000000000022001420425305200151550ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2014 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- import sys import nose if __name__ == "__main__": # By default, run tests in tests folder if len(sys.argv) == 1: sys.argv.append('tests') nose.main() liblarch-3.2.0/setup.py000066400000000000000000000051051420425305200150040ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- from distutils.core import setup import codecs import os def read(*parts): """ Build an absolute path from *parts* and and return the contents of the resulting file. Assume UTF-8 encoding. """ HERE = os.path.abspath(os.path.dirname(__file__)) with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: return f.read() setup( version='3.2.0', url='https://wiki.gnome.org/Projects/liblarch', author='Lionel Dricot & Izidor Matušov', author_email='gtg-contributors@lists.launchpad.net', license='LGPLv3', long_description=read("README.md"), long_description_content_type="text/markdown", name='liblarch', packages=['liblarch', 'liblarch_gtk'], python_requires=">=3.5", keywords = ["gtk", "treeview", "treemodel"], classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: X11 Applications :: GTK", "Intended Audience :: Developers", "Natural Language :: English", "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3.5", "Topic :: Desktop Environment :: Gnome", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: User Interfaces", ], description=( 'LibLarch is a python library built to easily handle ' 'data structures such as lists, trees and directed acyclic graphs ' 'and represent them as a GTK TreeWidget or in other forms.' ), ) liblarch-3.2.0/tests/000077500000000000000000000000001420425305200144335ustar00rootroot00000000000000liblarch-3.2.0/tests/__init__.py000066400000000000000000000000001420425305200165320ustar00rootroot00000000000000liblarch-3.2.0/tests/signals_testing.py000066400000000000000000000131141420425305200202020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- import threading import time from tests.watchdog import Watchdog from gi.repository import GLib class SignalCatcher(object): ''' A class to test signals ''' def __init__(self, unittest, generator, signal_name, should_be_caught=True, how_many_signals=1, error_code="No error code set"): self.signal_catched_event = threading.Event() self.generator = generator self.signal_name = signal_name self.signal_arguments = [] self.unittest = unittest self.how_many_signals = how_many_signals self.should_be_caught = should_be_caught self.error_code = error_code def _on_failure(): # we need to release the waiting thread self.signal_catched_event.set() self.missed = True # then we notify the error # if the error_code is set to None, we're expecting it to fail. if error_code is not None: print("An expected signal wasn't received %s" % error_code) self.unittest.assertFalse(should_be_caught) self.watchdog = Watchdog(3, _on_failure) def __enter__(self): def __signal_callback(*args): self.signal_arguments.append(args[1:]) if len(self.signal_arguments) >= self.how_many_signals: self.signal_catched_event.set() self.handler = self.generator.connect( self.signal_name, __signal_callback) self.watchdog.__enter__() return [self.signal_catched_event, self.signal_arguments] def __exit__(self, err_type, value, traceback): self.generator.disconnect(self.handler) if not self.should_be_caught and not hasattr(self, 'missed'): self.assertFalse(True) return (not isinstance(value, Exception) and self.watchdog.__exit__(err_type, value, traceback)) class CallbackCatcher(object): ''' A class to test callbacks ''' def __init__(self, unittest, generator, signal_name, should_be_caught=True, how_many_signals=1, error_code="No error code set"): self.signal_catched_event = threading.Event() self.generator = generator self.signal_name = signal_name self.signal_arguments = [] self.unittest = unittest self.how_many_signals = how_many_signals self.should_be_caught = should_be_caught self.error_code = error_code def _on_failure(): # we need to release the waiting thread self.signal_catched_event.set() self.missed = True # then we notify the error # if the error_code is set to None, we're expecting it to fail. if error_code is not None: print("An expected signal wasn't received %s" % error_code) self.unittest.assertFalse(should_be_caught) self.watchdog = Watchdog(3, _on_failure) def __enter__(self): def __signal_callback(*args): """ Difference to SignalCatcher is that we do not skip the first argument. The first argument by signals is widget which sends the signal -- we omit this feature when using callbacks """ self.signal_arguments.append(args) if len(self.signal_arguments) >= self.how_many_signals: self.signal_catched_event.set() self.handler = self.generator.register_cllbck( self.signal_name, __signal_callback) self.watchdog.__enter__() return [self.signal_catched_event, self.signal_arguments] def __exit__(self, err_type, value, traceback): self.generator.deregister_cllbck(self.signal_name, self.handler) if not self.should_be_caught and not hasattr(self, 'missed'): self.assertFalse(True) return (not isinstance(value, Exception) and self.watchdog.__exit__(err_type, value, traceback)) class GobjectSignalsManager(object): def init_signals(self): ''' Initializes the gobject main loop so that signals can be used. This function returns only when the gobject main loop is running ''' def gobject_main_loop(): self.main_loop = GLib.MainLoop() self.main_loop.run() threading.Thread(target=gobject_main_loop).start() while (not hasattr(self, 'main_loop') or not self.main_loop.is_running()): # since running the gobject main loop is a blocking call, # we have to check that it has been started in a polling fashion time.sleep(0.1) def terminate_signals(self): self.main_loop.quit() liblarch-3.2.0/tests/test_filteredtree.py000066400000000000000000000072021420425305200205230ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- import unittest from liblarch.treenode import _Node from liblarch.tree import MainTree from liblarch.filters_bank import FiltersBank from liblarch.filteredtree import FilteredTree class TestFilteredTree(unittest.TestCase): def setUp(self): self.added_nodes = 0 self.deleted_nodes = 0 self.tree = MainTree() self.filtersbank = FiltersBank(self.tree) self.filtered_tree = FilteredTree(self.tree, self.filtersbank) self.tree.add_node(_Node(node_id="apple")) self.tree.add_node(_Node(node_id="google")) self.filtered_tree.set_callback('deleted', self.deleted) self.filtered_tree.set_callback('added', self.added) def search_filter(self, node, parameters): return node.get_id() == parameters['node_id'] def true_filter(self, node): return True def test_refresh_every_time_with_parameters(self): self.filtersbank.add_filter("search_filter", self.search_filter) self.assertTrue(self.filtered_tree.is_displayed(node_id="apple")) self.assertTrue(self.filtered_tree.is_displayed(node_id="apple")) self.filtered_tree.apply_filter("search_filter", parameters={'node_id': 'apple'}) self.assertTrue(self.filtered_tree.is_displayed(node_id="apple")) self.assertFalse(self.filtered_tree.is_displayed(node_id="google")) # Due to self.refilter() implementation, all nodes are deleted # at first and then only those satisfying the filter are added back. self.assertEqual(2, self.deleted_nodes) self.assertEqual(1, self.added_nodes) self.reset_counters() self.filtered_tree.apply_filter("search_filter", parameters={'node_id': 'google'}) self.assertFalse(self.filtered_tree.is_displayed(node_id="apple")) self.assertTrue(self.filtered_tree.is_displayed(node_id="google")) self.assertEqual(1, self.deleted_nodes) self.assertEqual(1, self.added_nodes) def test_refresh_only_with_new_filter(self): self.filtersbank.add_filter("true_filter", self.true_filter) self.reset_counters() self.filtered_tree.apply_filter("true_filter") self.assertEqual(2, self.deleted_nodes) self.assertEqual(2, self.added_nodes) self.reset_counters() self.filtered_tree.apply_filter("true_filter") self.assertEqual(0, self.deleted_nodes) self.assertEqual(0, self.added_nodes) def added(self, node_id, path): self.added_nodes += 1 def deleted(self, node_id, path): self.deleted_nodes += 1 def reset_counters(self): self.added_nodes, self.deleted_nodes = 0, 0 liblarch-3.2.0/tests/test_liblarch.py000066400000000000000000002267301420425305200176360ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- """Tests for the tagstore.""" import unittest import gi # noqa gi.require_version('Gtk', '3.0') # noqa import functools import inspect import time import random from gi.repository import GLib from gi.repository import Gtk from gi.repository import GObject from liblarch import Tree from liblarch import TreeNode from liblarch_gtk import TreeView from tests.signals_testing import CallbackCatcher, GobjectSignalsManager from tests import signals_testing from tests.tree_testing import TreeTester # Prefer callbacks or signals? USE_CALLBACKS_INSTEAD_OF_SIGNALS = True class DummyNode(TreeNode): """ This is a dummy treenode that only have one properties: a color """ def __init__(self, tid): TreeNode.__init__(self, tid) self.colors = [] def add_color(self, color): if color not in self.colors: self.colors.append(color) self.modified() def has_color(self, color): return color in self.colors def remove_color(self, color): if color in self.colors: self.colors.remove(color) self.modified() # the tid should be the name of the filter used for the Ccount class CountNode(TreeNode): def __init__(self, tid, linked_tree): TreeNode.__init__(self, tid) self.tree = linked_tree viewcount = linked_tree.get_viewcount(name=tid) viewcount.apply_filter(tid) viewcount.register_cllbck(self.modified) class TestLibLarch(unittest.TestCase): """Tests for `Tree`.""" def caller_name(self): ''' Returns the filename and the line of the calling function. Precisely, it returns the calling calling function (because you're calling this one). ''' frame = inspect.currentframe() frame = frame.f_back.f_back code = frame.f_code return code.co_filename, code.co_firstlineno def _assertSignal(self, generator, signal_name, function, how_many_signals=1): def new(how_many_signals, error_code, *args, **kws): with signals_testing.SignalCatcher( self, generator, signal_name, how_many_signals=how_many_signals, error_code=error_code) \ as (signal_catched_event, signal_arguments): function(*args, **kws) signal_catched_event.wait() self.recorded_signals[signal_name] += signal_arguments return None # Functools.partial create something like a closure. It provides # several arguments by default, but additional arguments are # still possible return functools.partial(new, how_many_signals, self.caller_name()) def _assertCallback(self, generator, signal_name, function, how_many_signals=1): def new(how_many_signals, error_code, *args, **kws): with CallbackCatcher(self, generator, signal_name, how_many_signals=how_many_signals, error_code=error_code) \ as (signal_catched_event, signal_arguments): function(*args, **kws) signal_catched_event.wait() self.recorded_signals[signal_name] += signal_arguments return None return functools.partial(new, how_many_signals, self.caller_name()) if USE_CALLBACKS_INSTEAD_OF_SIGNALS: assertSignal = _assertCallback else: assertSignal = _assertSignal def test_assertSignal(self): """ Creates a fake GObject which emits N signals and test whether they are emitted. The last parameter of assertSignal(...)(33) is parameter for FakeGobject.emit_n_signals. """ class FakeGobject(GObject.GObject): __gsignals__ = { 'node-added-inview': (GObject.SignalFlags.RUN_FIRST, None, []) } def emit_n_signals(self, n): while n: n -= 1 GLib.idle_add(self.emit, 'node-added-inview') fake_gobject = FakeGobject() self._assertSignal(fake_gobject, 'node-added-inview', fake_gobject.emit_n_signals, 33)(33) def setUp(self): """Set up a dummy tree with filters and nodes. Construct a Tree for testing, with some filters for testing, including filters with parameters 'flat'. Create a collection of nodes with some of the properties these filters filter on. """ i = 0 # node numbers, used to check self.red_nodes = 0 self.blue_nodes = 0 self.green_nodes = 0 # Larch, is the tree. Learn to recognize it. self.tree = Tree() self.view = self.tree.get_viewtree() self.tester = TreeTester(self.view) self.mainview = self.tree.get_main_view() self.tree.add_filter('blue', self.is_blue) self.tree.add_filter('green', self.is_green) self.tree.add_filter('red', self.is_red) self.tree.add_filter('leaf', self.is_leaf) param = {} param['flat'] = True self.tree.add_filter('flatgreen', self.is_green, parameters=param) self.tree.add_filter('flatleaves', self.is_leaf, parameters=param) param = {} self.tree.add_filter('transblue', self.is_blue, parameters=param) self.tree.add_filter('transgreen', self.is_green, parameters=param) # first, we add some red nodes at the root while i < 5: node = DummyNode(str(i)) node.add_color('red') self.tree.add_node(node) i += 1 self.red_nodes += 1 # then, we add some blue nodes also at the root while i < 10: node = DummyNode(str(i)) node.add_color('blue') self.tree.add_node(node) i += 1 self.blue_nodes += 1 # finally, we add some green nodes as children of the last nodes # (stairs-like configuration) while i < 15: node = DummyNode(str(i)) node.add_color('green') self.tree.add_node(node, parent_id=str(i - 1)) i += 1 self.green_nodes += 1 self.total = self.red_nodes + self.blue_nodes + self.green_nodes # now testing the GTK treeview ################## # The columns description: desc = {} col = {} col['title'] = "Node name" render_text = Gtk.CellRendererText() col['renderer'] = ['markup', render_text] col['value'] = [str, lambda node: node.get_id()] desc['titles'] = col self.treeview = TreeView(self.view, desc) # initalize gobject signaling system self.gobject_signal_manager = GobjectSignalsManager() self.gobject_signal_manager.init_signals() self.recorded_signals = { 'node-added-inview': [], 'node-modified-inview': [], 'node-deleted-inview': [] } self.assertNodeAddedInview = functools.partial(self.assertSignal, self.view, 'node-added-inview') self.assertNodeModifiedInview = functools.partial( self.assertSignal, self.view, 'node-modified-inview') self.assertNodeDeletedInview = functools.partial( self.assertSignal, self.view, 'node-deleted-inview') def tearDown(self): try: self.tester.test_validity() finally: # stopping gobject main loop self.gobject_signal_manager.terminate_signals() # Filters def is_blue(self, node, parameters=None): return node.has_color('blue') def is_green(self, node, parameters=None): return node.has_color('green') def is_red(self, node, parameters=None): return node.has_color('red') def is_leaf(self, node, parameters=None): return not node.has_child() # Testing nodes movements in the tree # We test by counting nodes that meet some criterias def test_get_node(self): """Test that one node can be retrieved from the tree """ view = self.tree.get_viewtree() self.assertEqual(view.get_node_for_path(()), None) # we test that get node works for the last node node = self.tree.get_node(str(self.total - 1)) self.assertTrue(node is not None) self.assertEqual(str(self.total - 1), node.get_id()) # and not for an non-existing node self.assertRaises(ValueError, self.tree.get_node, str(self.total)) def test_add_remove_node(self): """ Test the adding and removal of nodes """ view = self.tree.get_viewtree(refresh=True) node = DummyNode('temp') node.add_color('blue') # run "self.tree.add_node(node, parent_id = '0')" and check that # it generates a 'node-added-view' AND a 'node-modified-inview' self.assertSignal( self.view, 'node-modified-inview', self.assertSignal(self.view, 'node-added-inview', self.tree.add_node))(node, parent_id='0') self.assertTrue( ('temp', ('0', 'temp')) in self.recorded_signals['node-added-inview']) self.assertTrue( ('0', ('0', )) in self.recorded_signals['node-modified-inview']) shouldbe = self.blue_nodes + 1 total = self.red_nodes + self.blue_nodes + self.green_nodes # Testing that the blue node count has increased self.assertEqual(total + 1, view.get_n_nodes()) self.assertEqual(shouldbe, view.get_n_nodes(withfilters=['blue'])) # also comparing with another view self.assertEqual(total + 1, self.view.get_n_nodes()) self.assertEqual(shouldbe, self.view.get_n_nodes(withfilters=['blue'])) self.tree.del_node('temp') # Testing that it goes back to normal self.assertEqual(total, view.get_n_nodes()) self.assertEqual(self.blue_nodes, view.get_n_nodes(withfilters=['blue'])) # also comparing with another view self.assertEqual(total, self.view.get_n_nodes()) self.assertEqual(self.blue_nodes, self.view.get_n_nodes(withfilters=['blue'])) self.tester.test_validity() def test_modifying_node(self): """ Modifying a node and see if the change is reflected in filters """ viewblue = self.tree.get_viewtree(refresh=False) testblue = TreeTester(viewblue) viewblue.apply_filter('blue') viewred = self.tree.get_viewtree(refresh=False) testred = TreeTester(viewred) viewred.apply_filter('red') node = DummyNode('temp') node.add_color('blue') # Do you see: we are modifying a child self.assertSignal(self.view, 'node-modified-inview', self.tree.add_node, 1)(node, parent_id='0') self.assertTrue( ('0', ('0', )) in self.recorded_signals['node-modified-inview']) # Node is blue self.assertTrue(viewblue.is_displayed('temp')) self.assertFalse(viewred.is_displayed('temp')) # node is blue and red node.add_color('red') self.assertTrue(viewblue.is_displayed('temp')) self.assertTrue(viewred.is_displayed('temp')) # node is red only node.remove_color('blue') self.assertFalse(viewblue.is_displayed('temp')) self.assertTrue(viewred.is_displayed('temp')) testred.test_validity() testblue.test_validity() def test_removing_parent(self): """Test behavior of node when its parent goes away. When you remove a parent, the child nodes should be added to the root if they don't have any other parents. """ self.tree.get_viewtree(refresh=True) node = DummyNode('temp') node.add_color('blue') self.tree.add_node(node, parent_id='0') all_nodes = self.view.get_all_nodes() self.assertTrue('0' in all_nodes) self.assertTrue('temp' in all_nodes) self.assertSignal(self.view, 'node-deleted-inview', self.tree.del_node, 1)('0') all_nodes = self.view.get_all_nodes() self.assertFalse('0' in all_nodes) self.assertTrue('temp' in all_nodes) def test_adding_to_late_parent(self): '''Add a node to a parent not yet in the tree then add the parent later''' view = self.tree.get_viewtree(refresh=True) node = DummyNode('child') self.tree.add_node(node, parent_id='futur') all_nodes = view.get_all_nodes() self.assertTrue('child' in all_nodes) self.assertFalse('futur' in all_nodes) self.assertEqual(len(view.node_parents('child')), 0) # now inserting the parent node2 = DummyNode('futur') self.tree.add_node(node2) all_nodes = view.get_all_nodes() self.assertTrue('child' in all_nodes) self.assertTrue('futur' in all_nodes) self.assertTrue('futur' in view.node_parents('child')) # TODO the same test but with filters def test_adding_to_late_parent2(self): '''Another tricky case with late parent. This was a very rare but existing crash''' view = self.tree.get_viewtree(refresh=True) node = DummyNode('child') self.tree.add_node(node) self.assertEqual(len(view.node_parents('child')), 0) node2 = DummyNode('futur') node.add_parent('futur') node.modified() self.assertEqual(len(view.node_parents('child')), 0) self.assertNotEqual(view.get_paths_for_node('child'), [(0, 0)]) self.tree.add_node(node2) self.assertTrue('futur' in view.node_parents('child')) def test_adding_to_late_parent3(self): '''Another tricky case with late parent. This was a very rare but existing crash''' view = self.tree.get_viewtree(refresh=True) view.apply_filter('red') node = DummyNode('child') node.add_color('red') self.tree.add_node(node) node2 = view.get_node('0') node2.remove_color('red') node.add_parent('0') node.modified() self.assertEqual(len(view.node_parents('child')), 0) self.assertNotEqual(view.get_paths_for_node('child'), [('0', 'child')]) node2.add_color('red') self.assertTrue('0' in view.node_parents('child')) def test_adding_self_parent(self): '''A node cannot be its own parent''' view = self.tree.get_viewtree(refresh=True) node = view.get_node('0') node.add_parent('0') self.assertEqual(len(node.get_parents()), 0) self.assertEqual(len(node.get_children()), 0) node.set_parent('0') self.assertEqual(len(node.get_parents()), 0) self.assertEqual(len(node.get_children()), 0) node.add_child('0') self.assertEqual(len(node.get_parents()), 0) self.assertEqual(len(node.get_children()), 0) def test_multiple_children(self): '''We test a node with two children.''' view = self.tree.get_viewtree(refresh=True) node = DummyNode('child') node2 = DummyNode('child2') self.tree.add_node(node, parent_id='0') self.tree.add_node(node2, parent_id='0') # We first test that the childrens are both there. self.assertEqual(view.node_n_children('0'), 2) self.assertEqual(view.next_node('child'), 'child2') # We build a list of children paths paths = [] paths += view.get_paths_for_node('child') paths += view.get_paths_for_node('child2') # take the paths of the parent - let's call it (X, ) " roots = view.get_paths_for_node('0') # Then, (X, 0) and (X, 1) should be both in paths of children for r in roots: p = r + ('child', ) self.assertTrue(p in paths) p = r + ('child2', ) self.assertTrue(p in paths) def test_counting_children(self): '''We test the number of children, recursively or not''' view = self.tree.get_viewtree(refresh=True) zero = self.tree.get_node('0') zero.add_color('blue') node = DummyNode('child') node.add_color('blue') node2 = DummyNode('child2') self.assertEqual(view.node_n_children('0'), 0) self.tree.add_node(node, parent_id='0') self.assertEqual(view.node_n_children('0'), 1) self.tree.add_node(node2, parent_id='0') self.assertEqual(view.node_n_children('0'), 2) self.assertEqual(view.node_n_children('0', recursive=True), 2) node3 = DummyNode('child3') node3.add_color('blue') self.tree.add_node(node3, parent_id='child') self.assertEqual(view.node_n_children('0'), 2) self.assertEqual(view.node_n_children('0', recursive=True), 3) node4 = DummyNode('child4') self.tree.add_node(node4, parent_id='child3') self.assertEqual(view.node_n_children('0'), 2) self.assertEqual(view.node_n_children('0', recursive=True), 4) self.assertEqual(view.node_n_children('child'), 1) self.assertEqual(view.node_n_children('child', recursive=True), 2) view.apply_filter('blue') self.assertEqual(view.node_n_children('0'), 1) self.assertEqual(view.node_n_children('0', recursive=True), 2) self.assertEqual(view.node_n_children('child'), 1) self.assertEqual(view.node_n_children('child', recursive=True), 1) node5 = DummyNode('child5') self.tree.add_node(node5, parent_id='child2') self.tree.del_node('child4') self.assertEqual(view.node_n_children('0'), 1) node2.add_color('blue') self.assertEqual(view.node_n_children('0', recursive=True), 3) view.unapply_filter('blue') self.assertEqual(view.node_n_children('0'), 2) self.assertEqual(view.node_n_children('0', recursive=True), 4) self.tree.del_node('child3') self.assertEqual(view.node_n_children('0'), 2) self.assertEqual(view.node_n_children('0', recursive=True), 3) def test_clean_multiple_parents(self): view = self.tree.get_viewtree(refresh=True) node = DummyNode('child') node2 = DummyNode('child2') self.tree.add_node(node, parent_id='0') self.tree.add_node(node2, parent_id='child') node.add_parent('1') node2.add_parent('1') self.assertEqual(len(view.node_parents('child')), 2) view.apply_filter('blue') def test_adding_to_late_parent_with_leaf_filter(self): '''Add a node to a parent not yet in the tree then add the parent later''' view = self.tree.get_viewtree(refresh=True) node = DummyNode('child') self.tree.add_node(node, parent_id='futur') all_nodes = view.get_all_nodes() self.assertTrue('child' in all_nodes) self.assertFalse('futur' in all_nodes) self.assertEqual(len(view.node_parents('child')), 0) # now inserting the parent view.apply_filter('leaf') node2 = DummyNode('futur') self.tree.add_node(node2) all_nodes = view.get_all_nodes() self.assertTrue('child' in all_nodes) self.assertFalse('futur' in all_nodes) self.assertFalse('futur' in view.node_parents('child')) view.reset_filters() self.assertTrue(view.is_displayed('futur')) self.assertTrue('futur' in view.node_parents('child')) def test_updating_parent(self): node = DummyNode('child') node.add_color('red') self.tree.add_node(node, parent_id='0') view = self.tree.get_viewtree(refresh=False) view.apply_filter('red') self.assertEqual(view.node_parents('child'), ['0']) path0 = view.get_paths_for_node('0')[0] pathchild = path0 + ('child', ) self.assertEqual(view.get_paths_for_node('child'), [pathchild]) node0 = view.get_node('0') node0.add_color('blue') self.assertEqual(view.node_parents('child'), ['0']) self.assertEqual(view.get_paths_for_node('child'), [pathchild]) node0.remove_color('red') self.assertEqual(view.node_parents('child'), []) self.assertEqual(view.get_paths_for_node('child'), [('child', )]) node0.add_color('red') path0 = view.get_paths_for_node('0')[0] pathchild = path0 + ('child', ) self.assertEqual(view.node_parents('child'), ['0']) self.assertEqual(view.get_paths_for_node('child'), [pathchild]) def test_addchild_with_late_parent(self): '''Add a child to a node which is not yet in the tree. We also check with a callback that the path sent is well corresponding to the nid received. ''' def check_path(nid, path): realnode = view.get_node_for_path(path) self.assertEqual(nid, realnode) def printtree(tid, paths=None): # The printtree method returns an error when the printed tree # is not logical. Thus, by connecting a print tree to signals, # the test will fail if there's any inconsistencies. view.print_tree(string=True) view = self.tree.get_viewtree(refresh=True) view.register_cllbck('node-modified-inview', check_path) view.register_cllbck('node-deleted-inview', printtree) node = DummyNode('child') node2 = DummyNode('futur') node3 = DummyNode('child2') node2.add_child('child') node2.add_child('child2') self.tree.add_node(node) self.tree.add_node(node3) all_nodes = view.get_all_nodes() self.assertTrue('child' in all_nodes) self.assertTrue('child2' in all_nodes) self.assertFalse('futur' in all_nodes) self.assertEqual(len(view.node_parents('child')), 0) self.assertEqual(len(view.node_parents('child2')), 0) # now inserting the parent view.apply_filter('leaf') self.tree.add_node(node2) all_nodes = view.get_all_nodes() self.assertTrue('child' in all_nodes) self.assertTrue('child2' in all_nodes) self.assertFalse('futur' in all_nodes) self.assertFalse('futur' in view.node_parents('child')) self.assertFalse('futur' in view.node_parents('child2')) view.reset_filters() self.assertTrue(view.is_displayed('futur')) self.assertTrue('futur' in view.node_parents('child')) self.assertTrue('futur' in view.node_parents('child2')) def test_addparent_with_late_child(self): '''Add a child not yet in the tree to a node''' view = self.tree.get_viewtree(refresh=True) node = DummyNode('parent') node2 = DummyNode('futur') node.add_child('futur') self.tree.add_node(node) all_nodes = view.get_all_nodes() self.assertTrue('parent' in all_nodes) self.assertFalse('futur' in all_nodes) self.assertEqual(view.node_n_children('parent'), 0) # now inserting the parent view.apply_filter('leaf') self.tree.add_node(node2) all_nodes = view.get_all_nodes() self.assertTrue('futur' in all_nodes) self.assertFalse('parent' in all_nodes) self.assertFalse('parent' in view.node_parents('futur')) view.reset_filters() self.assertTrue(view.is_displayed('parent')) self.assertTrue('futur' in view.node_all_children('parent')) def test_more_late_child(self): '''This one is trickier. We add a node with some children. Then, we add later a new child between the existing children. ''' view = self.tree.get_viewtree(refresh=True) node = DummyNode('parent') node1 = DummyNode('futur1') node2 = DummyNode('futur2') node3 = DummyNode('futur3') node4 = DummyNode('futur4') node.add_child('futur1') node.add_child('futur2') node.add_child('futur3') node.add_child('futur4') self.tree.add_node(node) self.tree.add_node(node1) # look, we miss the node 2 ! self.tree.add_node(node3) self.tree.add_node(node4) self.assertEqual(view.node_n_children('parent'), 3) self.tree.add_node(node2) self.assertEqual(view.node_n_children('parent'), 4) def test_late_first_child(self): '''Futur2 is the child of parent Futur1 is both the child of parent and futur2 Futur1 will be added later, forcing a reorganization. ''' view = self.tree.get_viewtree(refresh=True) node = DummyNode('parent') node1 = DummyNode('futur1') node2 = DummyNode('futur2') node.add_child('futur1') node.add_child('futur2') node2.add_child('futur1') self.tree.add_node(node) self.tree.add_node(node2) # Look, we didn't add futur1 self.assertEqual(view.node_n_children('parent'), 1) self.assertEqual(view.node_n_children('futur2'), 0) self.assertFalse(view.is_displayed('futur1')) # Now we add it ! self.tree.add_node(node1) self.assertEqual(view.node_n_children('parent'), 2) self.assertEqual(view.node_n_children('futur2'), 1) self.assertTrue(view.is_displayed('futur1')) def test_move_node_to_a_multiple_parent(self): view = self.tree.get_viewtree(refresh=True) node = self.tree.get_node('13') node3 = self.tree.get_node('3') node.add_parent('9') node.add_parent('10') self.tree.del_node('3') self.assertFalse(self.tree.has_node('3')) self.tree.add_node(node3, parent_id='13') self.assertEqual(len(view.get_paths_for_node('3')), 3) self.tree.del_node('3') self.tree.move_node('4', '13') self.assertEqual(len(view.get_paths_for_node('4')), 3) def test_recursive_removing_parent(self): """Test behavior of node when its parent goes away. When you remove a parent recursively, all the children are also removed ! """ self.tree.get_viewtree(refresh=True) node = DummyNode('temp') node.add_color('blue') self.tree.add_node(node, parent_id='0') all_nodes = self.view.get_all_nodes() self.assertTrue('0' in all_nodes) self.assertTrue('temp' in all_nodes) self.assertSignal(self.view, 'node-deleted-inview', self.tree.del_node, 1)('0', recursive=True) self.assertTrue( ('temp', ('0', 'temp')) in self.recorded_signals['node-deleted-inview']) all_nodes = self.view.get_all_nodes() self.assertFalse('0' in all_nodes) self.assertFalse('temp' in all_nodes) def test_move_node(self): """Test node movement from parents. Check that node can be moved from one node to another, and to root. When moved to root, verify it has no parents. """ view = self.tree.get_viewtree(refresh=True) node = DummyNode('temp') node.add_color('blue') self.tree.add_node(node, parent_id='0') # Testing initial situation self.assertTrue(view.node_has_child('0')) self.assertTrue('temp' in view.node_all_children('0')) self.assertTrue('temp' not in view.node_all_children('1')) # Moving node self.assertSignal(self.view, 'node-modified-inview', self.tree.move_node, 2)('temp', '1') self.assertTrue( ('1', ('1', )) in self.recorded_signals['node-modified-inview']) self.assertTrue(view.node_has_child('1')) self.assertTrue('temp' in view.node_all_children('1')) self.assertTrue('temp' not in view.node_all_children('0')) # Now moving to root self.tree.move_node('temp') self.assertTrue('temp' not in view.node_all_children('1')) self.assertTrue('temp' not in view.node_all_children('0')) # temp still exist and doesn't have any parents all_nodes = self.mainview.get_all_nodes() self.assertTrue('temp' in all_nodes) self.assertEqual(0, len(self.mainview.node_parents('temp'))) def test_add_parent(self): """Test that a node can have two parents. Verify that when a node with a parent gets a second parent, the node can be found in both parent nodes. """ view = self.tree.get_viewtree(refresh=True) node = DummyNode('temp') node.add_color('blue') self.assertSignal(self.view, 'node-modified-inview', self.tree.add_node, 1)(node, parent_id='0') # Not checking temp. Indeed, it has been added, so there should not # be any modified signal self.assertTrue( ('0', ('0', )) in self.recorded_signals['node-modified-inview']) # Testing initial situation self.assertTrue(view.node_has_child('0')) self.assertTrue('temp' in view.node_all_children('0')) self.assertTrue('temp' not in view.node_all_children('1')) # Adding another parent self.assertSignal(self.view, 'node-modified-inview', self.tree.add_parent, 1)('temp', '1') self.assertTrue( ('1', ('1', )) in self.recorded_signals['node-modified-inview']) self.assertTrue(view.node_has_child('1')) self.assertTrue('temp' in view.node_all_children('1')) self.assertTrue('temp' in view.node_all_children('0')) # we try to add a task as a child of one of its grand-children. # Nothing should happen def test_cyclic_paradox(self): """Try to add a node as a child of one of its grand-children.""" view = self.tree.get_main_view() node = DummyNode('temp') node.add_color('blue') self.tree.add_node(node, parent_id='0') self.tree.add_parent('0', '1') self.assertTrue('1' in self.mainview.node_parents('0')) self.assertTrue('0' in self.mainview.node_parents('temp')) # direct circular relationship self.assertRaises(Exception, self.tree.add_parent, '0', 'temp') # More complex circular relationship self.assertRaises(Exception, self.tree.add_parent, '1', 'temp') # And then printing => if it stops, nothing ciruclar stays there view.print_tree(True) def test_mainview(self): """Verify mainview behavior Test that mainview is always up-to-date and raise exception when trying to add filters on it """ self.assertRaises(Exception, self.mainview.apply_filter, 'blue') # Testing each method of the TreeView def test_viewtree_get_n_nodes(self): """ Test get_n_nodes() method of TreeView Check that retrieving counts of nodes with various filters returns the expected collections. """ total = self.red_nodes + self.blue_nodes + self.green_nodes self.assertEqual(total, self.view.get_n_nodes()) self.assertEqual(self.green_nodes, self.view.get_n_nodes(withfilters=['green'])) self.assertEqual(total, self.mainview.get_n_nodes()) def test_viewtree_get_n_nodes_with_cache(self): '''Testing the cache of the get_n_nodes''' nbr = self.green_nodes self.assertEqual(nbr, self.mainview.get_n_nodes(withfilters=['green'])) node = self.tree.get_node('0') node.add_color('green') self.assertEqual(nbr + 1, self.mainview.get_n_nodes(withfilters=['green'])) node.remove_color('green') self.assertEqual(nbr, self.mainview.get_n_nodes(withfilters=['green'])) def test_viewtree_get_all_nodes(self): all_nodes = self.view.get_all_nodes() all_nodes2 = self.mainview.get_all_nodes() self.assertEqual(True, '0' in all_nodes) self.assertEqual(False, 'tmp' in all_nodes) self.assertEqual(self.total, len(all_nodes)) # Mainview self.assertEqual(True, '0' in all_nodes2) self.assertEqual(False, 'tmp' in all_nodes2) self.assertEqual(self.total, len(all_nodes2)) # adding a node node = DummyNode('temp') node.add_color('blue') self.tree.add_node(node, parent_id=str(0)) all_nodes = self.view.get_all_nodes() all_nodes2 = self.mainview.get_all_nodes() self.assertTrue('0' in all_nodes) self.assertTrue('temp' in all_nodes) self.assertEqual(self.total + 1, len(all_nodes)) # Mainview self.assertTrue('0' in all_nodes2) self.assertTrue('temp' in all_nodes2) self.assertEqual(self.total + 1, len(all_nodes2)) # Removing the node self.tree.del_node('1') all_nodes = self.view.get_all_nodes() all_nodes2 = self.mainview.get_all_nodes() self.assertFalse('1' in all_nodes) self.assertTrue('temp' in all_nodes) self.assertEqual(self.total, len(all_nodes)) # mainview self.assertFalse('1' in all_nodes2) self.assertTrue('temp' in all_nodes2) self.assertEqual(self.total, len(all_nodes2)) def test_viewtree_get_node_for_path(self): view = self.tree.get_viewtree(refresh=True) # nid1 and nid2 are not always the same nid1 = view.get_node_for_path(('0', )) nid2 = self.mainview.get_node_for_path(('0', )) self.assertTrue(nid1 is not None) self.assertTrue(nid2 is not None) # Thus we do a mix of test. nid1b = view.next_node(nid1) path1b = view.get_paths_for_node(nid1b) self.assertEqual([(nid1b, )], path1b) # same for mainview nid2b = self.mainview.next_node(nid2) path2b = self.mainview.get_paths_for_node(nid2b) self.assertEqual([(nid2b, )], path2b) # with children node = DummyNode('temp') node.add_color('blue') self.tree.add_node(node, parent_id=nid1) self.tree.add_parent('temp', nid2) self.assertEqual('temp', self.mainview.get_node_for_path( (nid1, 'temp'))) self.assertEqual('temp', view.get_node_for_path((nid2, 'temp'))) # Adding a child to the child node2 = DummyNode('temp2') node2.add_color('blue') self.tree.add_node(node2, parent_id=nid1) node = DummyNode('temp_child') node.add_color('blue') self.tree.add_node(node, parent_id='temp2') self.assertEqual('temp_child', view.get_node_for_path((nid1, 'temp2', 'temp_child'))) self.tree.add_parent('temp2', nid2) self.assertEqual( 'temp_child', self.mainview.get_node_for_path((nid2, 'temp2', 'temp_child'))) # with filters view.apply_filter('blue') pl = view.get_paths_for_node('temp2') for p in pl: pp = p + ('temp_child', ) self.assertEqual('temp_child', view.get_node_for_path(pp)) def test_viewtree_get_paths_for_node(self): view = self.tree.get_viewtree(refresh=True) # testing the root path self.assertEqual([()], view.get_paths_for_node()) self.assertEqual([()], self.mainview.get_paths_for_node()) # with children # the first blue node is: firstgreen = self.red_nodes + self.blue_nodes - 1 pp = view.get_paths_for_node(str(firstgreen))[0] i = 0 # Testing all the green nodes (that are in stairs) while i < self.green_nodes: returned = view.get_paths_for_node(str(firstgreen + i))[0] self.assertEqual(pp, returned) i += 1 pp += (str(firstgreen + i), ) # with filters view.apply_filter('green') pp = view.get_paths_for_node(str(firstgreen + 1))[0] i = 1 # Testing all the green nodes (that are in stairs) while i < self.green_nodes: returned = view.get_paths_for_node(str(firstgreen + i))[0] self.assertEqual(pp, returned) i += 1 pp += (str(firstgreen + i), ) def test_viewtree_next_node(self): view = self.tree.get_viewtree(refresh=True) """Test next_node() for TreeView. Add two nodes to a parent, then verify various ways of looking at the next node in the parent's list. """ node = DummyNode('temp') node.add_color('blue') node.add_color('green') self.tree.add_node(node, parent_id='0') view = self.tree.get_viewtree(refresh=True) node = DummyNode('temp2') node.add_color('red') self.tree.add_node(node, parent_id='0') # we give the pid self.assertEqual('temp2', view.next_node('temp', pid='0')) self.assertEqual('temp2', self.mainview.next_node('temp', pid='0')) # or we give not (should be the same here because only one parent) self.assertEqual('temp2', view.next_node('temp')) self.assertEqual('temp2', self.mainview.next_node('temp')) # next node for last node. self.assertEqual(None, view.next_node('temp2')) self.assertEqual(None, self.mainview.next_node('temp2')) # with filters, temp should not have any next node view.apply_filter('blue', refresh=False) view.apply_filter('green') self.assertEqual(None, view.next_node('temp')) def test_viewtree_node_has_child(self): view = self.tree.get_viewtree(refresh=True) """Test node_has_child() for TreeView Verify that TreeView's node_n_children()'s return changes after a node is added to an empty TreeView instance. """ node = DummyNode('temp') node.add_color('blue') self.assertFalse(view.node_has_child('0')) self.assertFalse(self.mainview.node_has_child('0')) # Adding the node to the tree self.tree.add_node(node, parent_id='0') self.assertTrue(view.node_has_child('0')) self.assertTrue(self.mainview.node_has_child('0')) def test_moving_to_future_parent(self): view = self.tree.get_viewtree(refresh=True) node = DummyNode('future_par') node2 = DummyNode('child') self.tree.add_node(node2, parent_id='0') self.assertTrue('child' in view.node_all_children('0')) node2.set_parent('future_par') self.assertFalse('child' in view.node_all_children('0')) self.assertFalse(node2.has_parent()) self.tree.add_node(node) self.assertTrue('child' in view.node_all_children('future_par')) self.assertTrue(node2.has_parent()) def test_viewtree_node_all_children(self): view = self.tree.get_viewtree(refresh=True) self.assertEqual(0, len(view.node_all_children('0'))) """Test node_all_children() for TreeView. We also test node_n_children here. Nearly the same method. """ # checking that 0 and 1 are in root self.assertTrue('0' in view.node_all_children()) self.assertTrue('1' in view.node_all_children()) self.assertTrue('0' in self.mainview.node_all_children()) self.assertTrue('1' in self.mainview.node_all_children()) node = DummyNode('temp') node.add_color('blue') # adding a new children self.tree.add_node(node, parent_id='0') self.assertEqual(1, view.node_n_children('0')) self.assertTrue('temp' in view.node_all_children('0')) self.assertEqual(1, self.mainview.node_n_children('0')) self.assertTrue('temp' in self.mainview.node_all_children('0')) # Testing with a filter view.apply_filter('red') self.assertFalse('temp' in view.node_all_children('0')) view.unapply_filter('red') # moving an existing children self.tree.move_node('1', '0') self.assertEqual(2, view.node_n_children('0')) self.assertTrue('1' in view.node_all_children('0')) self.assertFalse('1' in view.node_all_children()) self.assertEqual(2, self.mainview.node_n_children('0')) self.assertTrue('1' in self.mainview.node_all_children('0')) self.assertFalse('1' in self.mainview.node_all_children()) # removing a node self.tree.del_node('temp') self.assertEqual(1, view.node_n_children('0')) self.assertFalse('temp' in view.node_all_children('0')) self.assertEqual(1, self.mainview.node_n_children('0')) self.assertFalse('temp' in self.mainview.node_all_children('0')) # moving a node elsewhere self.tree.move_node('1') self.assertEqual(0, view.node_n_children('0')) self.assertFalse('1' in view.node_all_children('0')) self.assertEqual(0, self.mainview.node_n_children('0')) self.assertFalse('1' in self.mainview.node_all_children('0')) # checking that '1' is back in root self.assertTrue('1' in view.node_all_children()) self.assertTrue('1' in self.mainview.node_all_children()) def test_viewtree_node_nth_child(self): """Test node_nth_child() for TreeView. Verify that when retrieving a given child node, that it is returned, except when requesting a node not in the tree or that is not present due being filtered out. """ view = self.tree.get_viewtree(refresh=True) node = DummyNode('temp') node.add_color('blue') # Asking for a child that doesn't exist should raise an exception self.assertRaises(ValueError, view.node_nth_child, '0', 0) self.assertRaises(ValueError, self.mainview.node_nth_child, '0', 0) # Adding the node to the tree self.tree.add_node(node, parent_id='0') self.assertEqual('temp', view.node_nth_child('0', 0)) self.assertEqual('temp', self.mainview.node_nth_child('0', 0)) # Now with a filter view.apply_filter('red') self.assertRaises(ValueError, view.node_nth_child, '0', 0) def test_viewtree_node_parents(self): view = self.tree.get_viewtree(refresh=True) """Test node_parents() for TreeView. Verify that a node's parents can be retrieved, if it has any. Check that if a node has multiple parents, that both parents are returned. """ # Checking that a node at the root has no parents self.assertEqual([], view.node_parents('0')) self.assertEqual([], self.mainview.node_parents('0')) # Adding a child node = DummyNode('temp') node.add_color('blue') self.tree.add_node(node, parent_id='0') self.assertEqual(['0'], view.node_parents('temp')) self.assertEqual(['0'], self.mainview.node_parents('temp')) # adding a second node.add_child('0')parent self.tree.add_parent('temp', '1') self.assertEqual(['0', '1'], view.node_parents('temp')) self.assertEqual(['0', '1'], self.mainview.node_parents('temp')) # now with a filter view.apply_filter('blue') self.assertEqual([], view.node_parents('temp')) # if the node is not displayed, asking for parents will raise an error view.unapply_filter('blue') view.apply_filter('red') self.assertRaises(IndexError, view.node_parents, 'temp') def test_viewtree_is_displayed(self): view = self.tree.get_viewtree(refresh=True) """Test is_displayed() for TreeView. Verify that a node is shown as displayed once it's been added to the tree, but not if an active filter should be hiding it. """ node = DummyNode('temp') node.add_color('blue') self.assertFalse(view.is_displayed('temp')) self.assertFalse(self.mainview.is_displayed('temp')) # Adding the node to the tree self.tree.add_node(node, parent_id='0') self.assertTrue(view.is_displayed('temp')) self.assertTrue(self.mainview.is_displayed('temp')) view.apply_filter('blue') self.assertTrue(view.is_displayed('temp')) view.apply_filter('red') self.assertFalse(view.is_displayed('temp')) # Filters ########### def test_simple_filter(self): view = self.tree.get_viewtree(refresh=False) test = TreeTester(view) """Test use of filters to restrict nodes shown. When the 'red' filter is applied, only nodes with the 'red' color should be returned. Applying the 'blue' filter on top of that should result in no nodes, since there are no nodes with both 'red' and 'blue'. When two filters are applied, and the second one is removed, the result should be the same as if only the first one had been applied. When a node gains a color, check that it is filtered appropriately. When a displayed node is added to a non-displayed parent, it should still be displayed. """ view.apply_filter('red') test.test_validity() self.assertEqual(self.red_nodes, view.get_n_nodes()) self.assertEqual(self.red_nodes, view.get_n_nodes(withfilters=['red'])) self.assertEqual(0, view.get_n_nodes(withfilters=['blue'])) # Red nodes are all at the root self.assertEqual(self.red_nodes, view.node_n_children()) # applying another filter view.apply_filter('green') test.test_validity() self.assertEqual(0, view.get_n_nodes()) # unapplying the first filter view.unapply_filter('red') test.test_validity() self.assertEqual(self.green_nodes, view.get_n_nodes()) self.assertEqual(self.green_nodes, view.get_n_nodes(withfilters=['green'])) self.assertEqual(0, view.get_n_nodes(withfilters=['red'])) # There's only one green node at the root self.assertEqual(1, view.node_n_children()) # Modifying a node to make it red and green self.assertFalse(view.is_displayed('0')) node = view.get_node('0') node.add_color('green') # It should now be in the view self.assertTrue(view.is_displayed('0')) self.assertEqual(1, view.get_n_nodes(withfilters=['red'])) self.assertEqual(2, view.node_n_children()) # Now, we add a new node node = DummyNode('temp') node.add_color('green') self.tree.add_node(node) test.test_validity() # It should now be in the view self.assertTrue(view.is_displayed('temp')) self.assertEqual(3, view.node_n_children()) # We remove it self.tree.del_node('temp') test.test_validity() self.assertFalse(view.is_displayed('temp')) self.assertEqual(2, view.node_n_children()) # We add it again as a children of a non-displayed node self.tree.add_node(node, parent_id='1') test.test_validity() self.assertTrue(view.is_displayed('temp')) self.assertEqual(3, view.node_n_children()) # It should not have parent self.assertEqual(0, len(view.node_parents('temp'))) def test_leaf_filter(self): view = self.tree.get_viewtree(refresh=False) test = TreeTester(view) """Test filtering to show only the leaf nodes. When the 'leaf' filter is applied and a child added to a node, the parent node should not be present in the results. """ view.apply_filter('leaf') total = self.red_nodes + self.blue_nodes self.assertEqual(total, view.get_n_nodes()) view.apply_filter('green') self.assertEqual(1, view.get_n_nodes()) nid = view.get_node_for_path(('14', )) # Now, we add a new node node = DummyNode('temp') node.add_color('green') self.tree.add_node(node, parent_id='14') self.assertEqual(1, view.get_n_nodes()) nid = view.get_node_for_path(('temp', )) self.assertEqual('temp', nid) # Only one node should be there self.assertEqual(1, view.get_n_nodes()) # 14 should not be there self.assertFalse(view.is_displayed('14')) # Now we remove 'temp' from the tree self.tree.del_node('temp') self.assertEqual(1, view.get_n_nodes()) self.assertTrue(view.is_displayed('14')) test.test_validity() def test_green_leaf(self): """We apply a green and leaf filter then we remove the green from one of the leaf node and the green parent should be displayed""" def green_leaf(node): # We return False if one child is green for c in node.get_children(): cnode = self.tree.get_node(c) if cnode.has_color('green'): return False return node.has_color('green') self.tree.add_filter('green_leaf', green_leaf) view = self.tree.get_viewtree(refresh=False) test = TreeTester(view) view.apply_filter('green_leaf') self.assertEqual(1, view.get_n_nodes()) nid = view.get_node_for_path(('14', )) # Now, we add a new node node = DummyNode('temp') node.add_color('green') self.tree.add_node(node, parent_id='14') self.assertEqual(1, view.get_n_nodes()) nid = view.get_node_for_path(('temp', )) self.assertEqual('temp', nid) # Only one node should be there self.assertEqual(1, view.get_n_nodes()) # 14 should not be there self.assertFalse(view.is_displayed('14')) # Now we remove 'temp' from the tree node.remove_color('green') self.assertEqual(1, view.get_n_nodes()) self.assertTrue(view.is_displayed('14')) test.test_validity() # we copy/paste the test def test_flatleaves_filters(self): """We apply a leaves + flat filter and the result should be the same as a simple leaf filter. """ view = self.tree.get_viewtree(refresh=False) test = TreeTester(view) view.apply_filter('flatleaves') total = self.red_nodes + self.blue_nodes self.assertEqual(total, view.get_n_nodes()) view.apply_filter('green') self.assertEqual(1, view.get_n_nodes()) nid = view.get_node_for_path(('14', )) # Now, we add a new node node = DummyNode('temp') node.add_color('green') self.tree.add_node(node, parent_id=nid) self.assertEqual(1, view.get_n_nodes()) nid = view.get_node_for_path(('temp', )) self.assertEqual('temp', nid) test.test_validity() # green are stairs # the flat filter should make them flat def test_flat_filters(self): """Test a flat filter. Green nodes are in "stairs" (each one being the child of another) By applying a filter with the flat properties, we test that all the nodes are now seen "flately". """ view = self.tree.get_viewtree(refresh=False) test = TreeTester(view) view.apply_filter('flatgreen') # all green nodes should be visibles self.assertEqual(self.green_nodes, view.get_n_nodes()) i = 10 nodes = [] # we check that the paths are on the root while i < self.green_nodes: nid = view.get_node_for_path((str(i), )) view.print_tree() nodes.append(nid) self.assertFalse(nid is None) # let see if a node has parent self.assertFalse(view.node_has_parent(nid)) # and, of course, it cannot have children self.assertFalse(view.node_has_child(nid)) i += 1 # we check that we have seen all the nodes i = 10 while i <= self.green_nodes: self.assertTrue(str(self.total - i) in nodes) i += 1 test.test_validity() def test_view_signals(self): self.tree.get_viewtree(refresh=True) def test_update_callback(self): '''We test the update callbacks and we check that the path received is well corresponding to the nid received''' def check_path(nid, path): self.assertEqual(view.get_node_for_path(path), nid) self.assertTrue(path in view.get_paths_for_node(nid)) view = self.tree.get_viewtree(refresh=False) test = TreeTester(view) view.register_cllbck('node-modified-inview', check_path) view.register_cllbck('node-added-inview', check_path) view.apply_filter('leaf') view.unapply_filter('leaf') test.test_validity() def test_torture(self): '''This is a torture test, where we will do whatever we want in random order. ''' view = self.tree.get_viewtree(refresh=False) test = TreeTester(view) view.reset_filters(refresh=True) node = DummyNode('parent') node.add_child('1') node.add_child('3') node.add_child('5') node.add_child('7') node.add_child('9') node.add_child('11') self.assertEqual(node.get_id(), "parent") self.assertFalse(view.is_displayed('parent')) self.tree.add_node(node) test.test_validity() self.assertEqual(view.node_n_children('parent'), 6) self.assertEqual(node.get_id(), "parent") view.apply_filter('blue') test.test_validity() self.assertEqual(node.get_id(), "parent") self.assertFalse(view.is_displayed('parent')) node.add_color('blue') test.test_validity() self.assertTrue(view.is_displayed('parent')) self.assertEqual(view.node_n_children('parent'), 3) def test_copypasting_child(self): view = self.tree.get_viewtree(refresh=False) test = TreeTester(view) view.apply_filter('green') node = DummyNode('child') node.add_color('green') node1 = DummyNode('child2') node1.add_color('green') node2 = DummyNode('parent') node2.add_color('green') self.tree.add_node(node2) self.tree.add_node(node, parent_id='10') self.tree.add_node(node1, parent_id='10') # We copy paste 'child' into 'parent' node2.add_child('child') test.test_validity() def test_speed(self): ''' Performance tests. Patches that reduce performance too much are not acceptable ''' self.tester.quit() BIG_NUMBER = 1000 nodes_id = [] start = time.time() for index in range(BIG_NUMBER): node = DummyNode("stress" + str(index)) nodes_id.append(node.get_id()) self.tree.add_node(node) end = time.time() print("\nADDING %d NODES: %f" % (BIG_NUMBER, end - start)) start = time.time() for node_id in nodes_id: self.tree.refresh_node(node_id) end = time.time() print("\nUPDATING %d NODES: %f" % (BIG_NUMBER, end - start)) start = time.time() for node_id in nodes_id: self.tree.del_node(node_id) end = time.time() print("\nDELETING %d NODES: %f" % (BIG_NUMBER, end - start)) def test_remove_grand_grand_parent(self): """ Remove from tree a node which is parent of child which is also parent. Using example tree from setUp() root: 0 1 2 3 4 5 6 7 8 9 <- We want to remove this 10 11 12 13 14 The result should be root: 1 2 3 4 5 6 7 8 10 11 12 13 14 """ self.tree.del_node("9") # Testing expected parents => (node, [parents]) relationships = [ (0, []), (1, []), (2, []), (3, []), (4, []), (5, []), (6, []), (7, []), (8, []), (10, []), (11, [10]), (12, [11]), (13, [12]), (14, [13]), ] for node_id, parents in relationships: # Convert IDs to strings node_id = str(node_id) parents = [str(parent) for parent in parents] self.assertEqual( self.tree.get_node(node_id).get_parents(), parents) def test_put_orphan_to_root(self): """ Put orphan (a child of deleted parent) to root, not to the parent of the parent Using example tree from setUp() root: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 <- we remove this 14 The result should be root: 0 1 2 3 4 5 6 7 8 10 11 12 14 """ self.tree.del_node("13") orphan_node = self.tree.get_node("14") self.assertEqual(orphan_node.get_parents(), []) def test_delete_task_randomly(self): """ Create a "big" number of tasks and delete them in "random" order. Testability is done by having always the same seed for random number generator. """ # Fairly random number (Debian-like :-D) SEED_NUMBER = 4 ADD_NODES_TO_TREE = 20 BASE_ID = 100 original_state = random.getstate() random.seed(SEED_NUMBER) view = self.tree.get_viewtree() total_count = view.get_n_nodes() parent_id = None for i in range(ADD_NODES_TO_TREE): node_id = str(BASE_ID + i) node = TreeNode(node_id, parent_id) self.tree.add_node(node) parent_id = node_id self.assertEqual(total_count + ADD_NODES_TO_TREE, view.get_n_nodes()) nodes = view.get_all_nodes() random.shuffle(nodes) for node_id in nodes: self.tree.del_node(node_id) random.setstate(original_state) def test_add_existing_relationship(self): """ Add the same relationship several times. LibLarch should recognize that the relationship exists and do nothing. """ view = self.tree.get_viewtree() count = view.get_n_nodes() parent_id = "parent" parent = DummyNode(parent_id) parent.add_parent("12") parent.add_parent("0") self.tree.add_node(parent) count += 1 self.assertEqual(count, view.get_n_nodes()) child_id = "child" child = DummyNode(child_id) self.tree.add_node(child) count += 1 # A random number of iterations for i in range(20): child.add_parent(parent_id) self.assertEqual(count, view.get_n_nodes()) self.assertEqual([parent_id], view.node_parents(child_id)) self.assertEqual([child_id], view.node_all_children(parent_id)) self.assertEqual(["0", "12"], sorted(view.node_parents(parent_id))) def test_remove_parent_of_multiple_parent_task_child(self): """ Remove one parent of multiple parent task child. The child should stay where it is, not moved to root. """ view = self.tree.get_viewtree() self.tree.add_node(DummyNode("t1")) self.tree.add_node(DummyNode("t2")) node = DummyNode("t3") node.add_parent("t1") self.tree.add_node(node) node = DummyNode("t4") node.add_parent("t3") node.add_parent("t2") self.tree.add_node(node) node = DummyNode("t5") node.add_parent("t4") self.tree.add_node(node) node = DummyNode("t6") node.add_parent("t1") self.tree.add_node(node) self.assertEqual(["t3", "t6"], view.node_all_children("t1")) self.assertEqual(["t4"], view.node_all_children("t2")) self.assertEqual(["t3", "t2"], view.node_parents("t4")) self.assertEqual(["t4"], view.node_parents("t5")) self.tree.del_node("t3") self.assertEqual(["t6"], view.node_all_children("t1")) self.assertEqual(["t4"], view.node_all_children("t2")) self.assertEqual(["t2"], view.node_parents("t4")) self.assertEqual(["t4"], view.node_parents("t5")) def test_remove_parent_of_multiple_children(self): """ Remove parent of multiple immediate children. This is very basic test but sometimes it fails. """ self.tree.add_node(DummyNode("A")) self.tree.add_node(DummyNode("B"), parent_id="A") self.tree.add_node(DummyNode("C"), parent_id="A") self.tree.del_node("A") def test_add_children_first(self): """ Set children of a task first and only then add it tree. This is the way the localfile backend works. """ CHILDREN_NUM = 6 children = ['%d@1' % i for i in range(1, CHILDREN_NUM + 1)] master_id = '0@1' view = self.tree.get_main_view() # We need to access root of the tree tree_root = view.get_node('root') # Clean tree first for node_id in view.get_all_nodes(): self.tree.del_node(node_id) self.assertEqual([], view.get_all_nodes()) self.assertEqual([], tree_root.get_children()) # Add master node with reference of children master = DummyNode(master_id) for child_id in children: master.add_child(child_id) self.tree.add_node(master) # Now add children for child_id in children: self.tree.add_node(DummyNode(child_id)) # Check status self.assertEqual([master_id] + children, sorted(view.get_all_nodes())) # Master node self.assertEqual([], view.node_parents(master_id)) self.assertEqual(children, view.node_all_children(master_id)) # Children for node_id in children: self.assertEqual([master_id], view.node_parents(node_id)) self.assertEqual([], view.node_all_children(node_id)) # Check root => there should be no nodes but master self.assertEqual([master_id], tree_root.get_children()) def test_node_count_update(self): """this is for the @t @ta @tag bug""" def filter_func(node): # we display a color in tagtree only if # this color has at least one node in self.tree color = node.get_id() viewcount = self.tree.get_viewcount(name=color, refresh=True) count = viewcount.get_n_nodes() return count > 0 # self.tree is where we will store "tasks" (here colors) # the main tree will be the tag tree. tagtree = Tree() blue_tag = CountNode("blue", self.tree) tagtree.add_node(blue_tag) green_tag = CountNode("green", self.tree) tagtree.add_node(green_tag) red_tag = CountNode("red", self.tree) tagtree.add_node(red_tag) tagtree.add_filter("color_exists", filter_func) view = tagtree.get_viewtree() view.apply_filter("color_exists") self.assertTrue(view.is_displayed("red")) self.assertTrue(view.is_displayed("blue")) self.assertTrue(view.is_displayed("green")) self.tree.del_node('14') self.tree.del_node('13') self.tree.del_node('12') self.tree.del_node('11') self.tree.del_node('10') self.tree.del_node('9') self.tree.del_node('8') self.assertTrue(view.is_displayed("red")) self.assertTrue(view.is_displayed("blue")) # there are no remaining green nodes self.assertFalse(view.is_displayed("green")) # print self.tree.get_viewtree().print_tree(True) # print view.print_tree(True) # for color in ['red', 'blue', 'green']: # count = self.view.get_n_nodes(withfilters=[color]) # print "%s %s nodes" %(count, color) def test_maintree_print_tree(self): """ Test MainTree's print_tree() to string """ view = self.tree.get_main_view() self.assertEqual( view.print_tree(True), """root 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 """) self.tree.add_node(DummyNode('temp'), '0') self.assertEqual(['temp'], view.node_all_children('0')) self.assertEqual( view.print_tree(True), """root 0 temp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 """) def test_almost_circular_dependencies(self): """ Have the nasty tree: n1 -n2 -n3 --n2 """ a = DummyNode("a") b = DummyNode("b") c = DummyNode("c") a.add_child("b") a.add_child("c") c.add_child("b") self.tree.add_node(a) self.tree.add_node(b) self.tree.add_node(c) def test_remove_tasks(self): """ This test case is based on real code and a bug. have node 'a' with children 'b', 'c' and then remove 'a' recursively. """ a = DummyNode("a") b = DummyNode("b") c = DummyNode("c") a.add_child("b") a.add_child("c") self.tree.add_node(a) self.tree.add_node(b) self.tree.add_node(c) for node_id in ['a', 'b', 'c']: if self.tree.has_node(node_id): self.tree.del_node(node_id, True) def test_remove_recursively_clean(self): """ Test that when we have task with many subtasks, all will be removed and no will left in tree """ N = 50 prefix = "child_" view = self.tree.get_main_view() parent = DummyNode("parent") self.tree.add_node(parent) for i in range(N): node_id = prefix + str(i) self.tree.add_node(DummyNode(node_id)) parent.add_child(node_id) self.tree.del_node("parent", True) self.assertTrue("parent" not in view.get_all_nodes()) # No orphans are left for node_id in view.get_all_nodes(): self.assertFalse(node_id.startswith(prefix)) def test_queue_action_one_action(self): self.testvalue = 0 def action(x): self.testvalue += x self.tree = Tree() self.view = self.tree.get_viewtree() self.tree.add_filter('blue', self.is_blue) self.tree.add_filter('green', self.is_green) self.view.apply_filter('green') bl = DummyNode('bl') bl.add_color('blue') gr = DummyNode('gr') gr.add_color('green') self.view.queue_action('bl', action, 1) self.view.queue_action('gr', action, 2) self.assertEqual(self.testvalue, 0) self.tree.add_node(bl) self.assertEqual(self.testvalue, 0) self.tree.add_node(gr) self.assertEqual(self.testvalue, 2) def test_queue_action_multiples_actions(self): self.testvalue = 0 def action(x): self.testvalue += x self.tree = Tree() self.view = self.tree.get_viewtree() self.tree.add_filter('blue', self.is_blue) self.tree.add_filter('green', self.is_green) self.view.apply_filter('green') bl = DummyNode('bl') bl.add_color('blue') gr = DummyNode('gr') gr.add_color('green') self.view.queue_action('bl', action, 1) self.view.queue_action('bl', action, 3) self.view.queue_action('gr', action, 2) self.assertEqual(self.testvalue, 0) self.tree.add_node(bl) self.assertEqual(self.testvalue, 0) self.tree.add_node(gr) self.assertEqual(self.testvalue, 2) self.view.unapply_filter('green') # test value should be 2 + 1 + 3 = 6 self.assertEqual(self.testvalue, 6) def test_recursive_count(self): self.value = 0 self.view = self.tree.get_viewtree() def update(x, path): if x == '0': self.value = self.view.node_n_children(x, recursive=True) self.view.register_cllbck('node-modified-inview', update) a = DummyNode('a') b = DummyNode('b') c = DummyNode('c') d = DummyNode('d') d.add_color('blue') zero = self.tree.get_node('0') zero.add_color('blue') self.assertEqual(self.value, 0) self.tree.add_node(a, '0') self.assertEqual(self.value, 1) self.tree.add_node(b, 'a') self.assertEqual(self.value, 2) self.tree.add_node(c, 'b') self.assertEqual(self.value, 3) self.tree.add_node(d, '0') self.assertEqual(self.value, 4) self.tree.del_node('b') self.assertEqual(self.value, 2) self.view.apply_filter('blue') self.assertEqual(self.value, 1) def test_performance_of_filter_counting(self): """ Simulate counting tags as in GTG use case. For every update in main tree, recount every tag filter """ # FIXME # Super exterme task count # Optimal time is 0.917s TASK_COUNT = 2000 # Optimal time (with return 0) is 0.321s TASK_COUNT = 500 # FIXME I had to lower task count # Optimal time is 0.084s TASK_COUNT = 200 PROP_OF_CHILDREN = 25 TOTAL_TAGS = 40 MAX_TAGS_PER_TASK = 4 random.seed(42) def create_filter_func(tag): """ We need to create a closure to generate filter functions """ return lambda node, parameters=None: node.has_color(tag) # add filters tags = ['@t%03d' % i for i in range(TOTAL_TAGS)] num_tags = dict((tag, 0) for tag in tags) for tag in tags: self.tree.add_filter(tag, create_filter_func(tag)) def recount(node, path): """ Make all tags to recount """ for tag in tags: count = view.get_n_nodes(withfilters=[tag]) self.assertEqual(count, num_tags[tag]) # register callback view = self.tree.get_viewtree() view.register_cllbck('node-added-inview', recount) view.register_cllbck('node-modified-inview', recount) view.register_cllbck('node-deleted-inview', recount) parents = [] for task in range(TASK_COUNT): task_id = 'task_%d' % task node = DummyNode(task_id) tag_count = random.randint(0, MAX_TAGS_PER_TASK) for tag in random.sample(tags, tag_count): num_tags[tag] += 1 node.add_color(tag) if random.randint(0, 100) < PROP_OF_CHILDREN: if parents != []: parent_id = random.choice(parents) else: parent_id = None parents.append(task_id) else: parent_id = None parents = [] self.tree.add_node(node, parent_id=parent_id) def test_modifying_child_of_removed_node(self): """ Removing tasks with a recursive filter throws a traceback, see this bug report: https://bugs.launchpad.net/gtg/+bug/932405 """ def is_there_purple_somewhere(node, parameters=None): if node.has_color('purple'): return True for child_id in node.get_children(): child = self.tree.get_node(child_id) if is_there_purple_somewhere(child): return True return False self.tree.add_filter('is_there_purple_somewhere', is_there_purple_somewhere) self.view.apply_filter('is_there_purple_somewhere') parent = DummyNode('parent') self.tree.add_node(parent) child = DummyNode('child') child.add_color('purple') self.tree.add_node(child, 'parent') child.modified() self.assertEqual(self.view.node_all_children('parent'), ['child']) # During this call, there should be no traceback child.remove_color('purple') def test_late_add_recursive_filter(self): """ Nodes sometimes could be just meta nodes. Think about tags in GTG which have just sub-tags and have no actual tasks. They should be still shown and their task count is counted as a count of all tasks associated with any subtask. This test have simple tree where @a is a node and @b its subnode. The view shows only pink nodes or nodes which have a pink ancestor. After the view is created, we add "pink" to @b and it should show tree @a -> @b. At the writing of this test, there was a bug where you get @a, @b instead. Calling additional b.modified() solved the situation. """ def has_something_pink(node, parameters=None): if node.has_color('pink'): return True else: for child_id in node.get_children(): child = self.tree.get_node(child_id) if has_something_pink(child, parameters): return True return False a = DummyNode('@a') self.tree.add_node(a) b = DummyNode('@b') self.tree.add_node(b, '@a') self.tree.add_filter('has_something_pink', has_something_pink) view = self.tree.get_viewtree('pinktree') view.apply_filter('has_something_pink') b.add_color('pink') self.assertEqual(set(view.get_all_nodes()), set(['@a', '@b'])) self.assertEqual(view.node_all_children(), ['@a']) self.assertEqual(view.node_all_children('@a'), ['@b']) def test_deleted_callback_has_correct_nodes_count(self): """ Test case for LP #1078368 """ def update_count(*args): """ Check if node count is equal to one from view's interface """ self.assertEqual(node_count, self.view.get_n_nodes()) self.view.register_cllbck('node-deleted-inview', update_count) node_count = self.view.get_n_nodes() for node_id in list(self.view.get_nodes()): if not self.view.node_has_child(node_id): node_count -= 1 self.tree.del_node(node_id) def test_crash_when_collapsing_future_nodes(self): """ Due to a mistake trying to collapse a node that isn't in the tree yet causes it to schedule that operation for later, except with the wrong argument type, making PyGObject complain. """ self.treeview.collapse_node(('123123123123',)) # Should schedule self.tree.add_node(DummyNode('123123123123')) # Would've errored # TypeError: argument path: Expected Gtk.TreePath, but got tuple def test_crash_when_expanding_future_nodes(self): """ Due to a mistake trying to expand a node that isn't in the tree yet causes it to schedule that operation for later, except with the wrong argument type, making PyGObject complain. """ self.treeview.expand_node(('123123123123',)) # Should schedule self.tree.add_node(DummyNode('123123123123')) # Would've errored # TypeError: argument path: Expected Gtk.TreePath, but got tuple liblarch-3.2.0/tests/test_signal_testing.py000066400000000000000000000042341420425305200210610ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- import unittest from gi.repository import GObject import uuid from gi.repository import GLib from tests.signals_testing import SignalCatcher, GobjectSignalsManager class TestSignalTesting(unittest.TestCase): def setUp(self): self.gobject_signal_manager = GobjectSignalsManager() self.gobject_signal_manager.init_signals() def tearDown(self): self.gobject_signal_manager.terminate_signals() def test_signal_catching(self): generator = FakeGobject() arg = str(uuid.uuid4()) with SignalCatcher(self, generator, 'one') as [signal_catched_event, signal_arguments]: generator.emit_signal('one', arg) signal_catched_event.wait() self.assertEqual(len(signal_arguments), 1) self.assertEqual(len(signal_arguments[0]), 1) one_signal_arguments = signal_arguments[0] self.assertEqual(arg, one_signal_arguments[0]) class FakeGobject(GObject.GObject): __gsignals__ = { 'one': (GObject.SignalFlags.RUN_FIRST, None, (str, )), 'two': (GObject.SignalFlags.RUN_FIRST, None, (str, )), } def emit_signal(self, signal_name, argument): GLib.idle_add(self.emit, signal_name, argument) liblarch-3.2.0/tests/tree_testing.py000066400000000000000000000220251420425305200175020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- # If True, the TreeTester will automatically reorder node on the same level # as a deleted node. If False, it means that Liblarch has the responsability # to handle that itself. REORDER_ON_DELETE = False class TreeTester(object): '''A class that will check if a tree implementation is consistent by connecting to emitted signals and crashing on any problem''' def __init__(self, viewtree): self.tree = viewtree # both dict should always be synchronized # They are the internal representation of the tree, # based only on received signals self.nodes = {} self.paths = {} self.tree.register_cllbck('node-added-inview', self.add) self.tree.register_cllbck('node-deleted-inview', self.delete) self.tree.register_cllbck('node-modified-inview', self.update) self.tree.register_cllbck('node-children-reordered', self.reordered) self.trace = "* * * * * * * *\n" def add(self, nid, path): self.trace += "adding %s to path %s\n" % (nid, str(path)) currentnode = self.paths.get(path, None) if currentnode and currentnode != nid: raise Exception( 'path %s is already occupied by %s' % (str(path), nid)) if nid in self.nodes: node = self.nodes[nid] else: node = [] self.nodes[nid] = node if path not in node: node.append(path) self.paths[path] = nid def delete(self, nid, path): self.trace += "removing %s from path %s\n" % (nid, str(path)) if nid != self.paths.get(path, None): error = '%s is not assigned to path %s\n' % (nid, str(path)) error += self.print_tree() raise Exception(error) if path not in self.nodes.get(nid, []): raise Exception('%s is not a path of node %s' % (str(path), nid)) if REORDER_ON_DELETE: index = path[-1:] print("reorder on delete not yet implemented") self.nodes[nid].remove(path) if len(self.nodes[nid]) == 0: self.nodes.pop(nid) self.paths.pop(path) # Move other paths lower like in real TreeModel path_prefix = path[:-1] index = path[-1] assert path_prefix + (index, ) == path, ( "%s vs %s" % (path_prefix + (index, ), path)) def check_prefix(path): """ Is this path affected by the change? Conditions: * the same prefix (3, 1, 2, 3) vs (3, 1, 2, 4) OK (3, 1, 2, 3) vs (3, 1, 2, 4, 0) OK (3, 1, 2, 3) vs (3, 2, 2, 4) FALSE * higher index (3, 1, 2, 3) vs (3, 1, 2, 2) FALSE """ if len(path) <= len(path_prefix): return False for i, pos in enumerate(path_prefix): if path[i] != pos: return False return path[len(path_prefix)] > index paths = list(self.paths.keys()) paths.sort() for path in paths: old_path = path if check_prefix(path) and len(path_prefix) > 1: new_path = list(path) print("new_path: %s" % str(new_path)) new_path[len(path_prefix)] = str(int( new_path[len(path_prefix)]) - 1) new_path = tuple(new_path) print("new_path: %s" % str(new_path)) print("self.paths: %s" % str(self.paths)) assert new_path not in self.paths nid = self.paths[old_path] self.nodes[nid].remove(old_path) del self.paths[old_path] self.nodes[nid].append(new_path) self.paths[new_path] = nid def update(self, nid, path): # Because of the asynchronousness of update, this test # doesn't work anymore pass # self.tree.flush() # self.trace += "updating %s in path %s\n" % (nid, str(path)) # error = "updating node %s for path %s\n" % (nid, str(path)) # if not self.nodes.has_key(nid): # error += "%s is not in nodes !\n" %nid # error += self.print_tree() # raise Exception(error) # # Nothing to do, we just update. # for p in self.nodes[nid]: # if self.paths[p] != nid: # raise Exception('Mismatching path for %s'%nid) # if not self.paths.has_key(path): # error += '%s is not in stored paths (node %s)\n'% ( # str(path), nid) # error += self.print_tree() # raise Exception(error) # n = self.paths[path] # if path not in self.nodes[n] or n != nid: # raise Exception('Mismatching node for path %s'%str(p)) def reordered(self, nid, path, neworder): self.trace += "reordering children of %s (%s) : %s\n" % ( nid, str(path), neworder) self.trace += "VR is %s\n" % self.tree.node_all_children() if not path: path = () i = 0 newpaths = {} toremove = [] # we first update self.nodes with the new paths while i < len(neworder): if i != neworder[i]: old = neworder[i] oldp = path + (old, ) newp = path + (i, ) le = len(newp) for pp in list(self.paths.keys()): if pp[0:le] == oldp: n = self.paths[pp] self.nodes[n].remove(pp) newpp = newp + pp[le:] self.nodes[n].append(newpp) self.trace += " change %s path from %s to %s\n" % ( n, pp, newpp) newpaths[newpp] = n toremove.append(pp) i += 1 # now we can update self.paths for p in toremove: self.paths.pop(p) for p in newpaths: self.trace += " adding %s to paths %s\n" % (newpaths[p], str(p)) self.paths[p] = newpaths[p] def test_validity(self): for n in list(self.nodes.keys()): paths = self.tree.get_paths_for_node(n) if len(self.nodes[n]) == 0: raise Exception('Node %s is stored without any path' % n) for p in self.nodes[n]: if self.paths[p] != n: raise Exception('Mismatching path for %s' % n) if p not in paths: error = 'we have a unknown stored path for %s\n' % n nn = self.tree.get_node_for_path(p) error += ' path %s is the path of %s\n' % ( str(p), str(nn)) error += ' parent is %s' % self.tree.get_node_for_path( p[:-1]) raise Exception(error) paths.remove(p) if len(paths) > 0: raise Exception('why is this path existing for %s' % n) for p in list(self.paths.keys()): node = self.tree.get_node_for_path(p) n = self.paths[p] if n != node: error = 'Node for path is %s but should be %s' % (node, n) raise Exception(error) if p not in self.nodes[n]: error = 'Mismatching node for path %s\n' % str(p) error += self.print_tree() raise Exception(error) if len(p) == 1 and len(self.nodes[n]) > 1: error = 'Node %s has multiple paths and is in the VR\n' % n error += self.print_tree() raise Exception(error) return True def print_tree(self): st = self.trace st += "nodes are %s\n" % self.nodes st += "paths are %s\n" % self.paths return st def quit(self): self.tree.deregister_cllbck('node-added-inview', self.add) self.tree.deregister_cllbck('node-deleted-inview', self.delete) self.tree.deregister_cllbck('node-modified-inview', self.update) self.tree.deregister_cllbck('node-children-reordered', self.reordered) liblarch-3.2.0/tests/watchdog.py000066400000000000000000000035651420425305200166160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Liblarch - a library to handle directed acyclic graphs # Copyright (c) 2011-2012 - Lionel Dricot & Izidor Matušov # # This program 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 3 of the License, or (at your option) any # later version. # # This program 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 program. If not, see . # ----------------------------------------------------------------------------- import threading class Watchdog(object): ''' a simple thread-safe watchdog. usage: with Watchdod(timeout, error_function): #do something ''' def __init__(self, timeout, error_function): ''' Just sets the timeout and the function to execute when an error occours @param timeout: timeout in seconds @param error_function: what to execute in case the watchdog timer triggers ''' self.timeout = timeout self.error_function = error_function def __enter__(self): '''Starts the countdown''' self.timer = threading.Timer(self.timeout, self.error_function) self.timer.start() def __exit__(self, type, value, traceback): '''Aborts the countdown''' try: self.timer.cancel() except: pass if value is None: return True return False