From faa8e960910f37fb2345f77314dac87d2947376d Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Fri, 23 Jan 2004 11:51:48 +0000 Subject: [PATCH] New tasks module. Fixed version test. git-svn-id: https://rox.svn.sourceforge.net/svnroot/rox/trunk/ROX-Lib2@3307 66de3db3-b00d-0410-b41b-f4738ad19bea --- Help/Changes | 15 +++- Help/findrox.py | 2 +- python/rox/__init__.py | 2 +- python/rox/tasks.py | 227 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 python/rox/tasks.py diff --git a/Help/Changes b/Help/Changes index 3a94bdb..f69b6b6 100644 --- a/Help/Changes +++ b/Help/Changes @@ -3,11 +3,21 @@ by Thomas Leonard http://rox.sourceforge.net -02-Jan-2003 +22-Jan-2004 ~~~~~~~~~~~ -Added method to MIMEtype to fetch its icon. Install list window for +Added 'tasks' module to provide a light-weight alternative to threads. +Bugfix: On non-zero-install systems, the sense of the version check in findrox + was wrong. + +02-Jan-2004 +~~~~~~~~~~~ +Added method to MIMEtype to fetch its icon. Install list window for the MIME handler installation uses it (Stephen Watson). +13-Dec-2003 +~~~~~~~~~~~ +Translate text in Options box. + 28-Dec-2003 ~~~~~~~~~~~ Allow ROXLIB_DISABLE_ZEROINSTALL to override looking for ROX-Lib in Zero @@ -21,6 +31,7 @@ made a few changes to the XML syntax. ~~~~~~~~~~~ Added OptionMenu widget for Options box (Chris Shaffer). +>>>>>>> 1.79 27-Nov-2003 ~~~~~~~~~~~ Fixed bug in findrox when not using Zero Install (Lionel Bringuier). diff --git a/Help/findrox.py b/Help/findrox.py index 0d58f6e..ea4442c 100644 --- a/Help/findrox.py +++ b/Help/findrox.py @@ -45,7 +45,7 @@ def version(major, minor, micro): return # Can't check version if not hasattr(rox, 'roxlib_version'): break - if (major, minor, micro) >= rox.roxlib_version: + if (major, minor, micro) <= rox.roxlib_version: return # OK report_error("This program needs ROX-Lib2 (version %d.%d.%d) " % \ (major, minor, micro) + "to run.\n" + \ diff --git a/python/rox/__init__.py b/python/rox/__init__.py index 045c3c8..a712950 100644 --- a/python/rox/__init__.py +++ b/python/rox/__init__.py @@ -33,7 +33,7 @@ import sys, os, codecs to_utf8 = codecs.getencoder('utf-8') -roxlib_version = (1, 9, 11) +roxlib_version = (1, 9, 12) _path = os.path.realpath(sys.argv[0]) app_dir = os.path.dirname(_path) diff --git a/python/rox/tasks.py b/python/rox/tasks.py new file mode 100644 index 0000000..5ee2ea7 --- /dev/null +++ b/python/rox/tasks.py @@ -0,0 +1,227 @@ +"""The tasks module provides a simple light-weight alternative to threads. + +THIS MODULE IS EXPERIMENTAL. Feedback on the API is appreciated. Things may +change in the next few versions, so watch out! + +When you have a long-running job you will want to run it in the background, +while the user does other things. There are four ways to do this: + +- Use a new thread for each task. +- Use callbacks from an idle handler. +- Use a recursive mainloop. +- Use this module. + +Using threads causes a number of problems. Some builds of pygtk/python don't +support them, they introduce race conditions, often lead to many subtle +bugs, and they require lots of resources (you probably wouldn't want 10,000 +threads running at once). In particular, two threads can run at exactly the +same time (perhaps on different processors), so you have to be really careful +that they don't both try to update the same variable at the same time. This +requires lots of messy locking, which is hard to get right. + +Callbacks work within a single thread. For example, you open a dialog box and +then tell the system to call one function if it's closed, and another if the +user clicks OK, etc. The function that opened the box then returns, and the +system calls one of the given callback functions later. Callbacks only +execute one at a time, so you don't have to worry about race conditions. +However, they are often very awkward to program with, because you have to +save state somewhere and then pass it to the functions when they're called. + +A recursive mainloop only works with nested tasks (you can create a +sub-task, but the main task can't continue until the sub-task has +finished). We use these for, eg, rox.alert() boxes since you don't +normally want to do anything else until the box is closed, but it is not +appropriate for long-running jobs. + +Tasks use python's generator API to provide a more pleasant interface to +callbacks. See the Task class (below) for more information. +""" + +from __future__ import generators + +import rox +from rox import g, _ + +# The list of Blockers whose event has happened, in the order they were +# triggered +_run_queue = [] + +class Blocker: + """A Blocker object starts life with 'happened = False'. Tasks can + ask to be suspended until 'happened = True'. The value is changed + by a call to trigger(). + + Example: + + kettle_boiled = tasks.Blocker() + + def make_tea(): + print "Get cup" + print "Add tea leaves" + yield kettle_boiled; + print "Pour water into cup" + print "Brew..." + yield tasks.TimeoutBlocker(120) + print "Add milk" + print "Ready!" + + tasks.Task(make_tea()) + + # elsewhere, later... + print "Kettle boiled!" + kettle_boiled.trigger() + + You can also yield a list of Blockers. Your function will resume + after any one of them is triggered. Use blocker.happened to + find out which one(s). Yielding a Blocker that has already + happened is the same as yielding None (gives any other Tasks a + chance to run, and then continues). + """ + + def __init__(self): + self.happened = False # False until event triggered + self._rox_lib_tasks = {} # Tasks waiting on this blocker + + def trigger(self): + """The event has happened. Note that this cannot be undone; + instead, create a new Blocker to handle the next occurance + of the event.""" + if self.happened: return # Already triggered + self.happened = True + #assert self not in _run_queue # XXX: Slow + if not _run_queue: + _schedule() + _run_queue.append(self) + + def add_task(self, task): + """Called by the schedular when a Task yields this + Blocker. If you override this method, be sure to still + call this method with Blocker.add_task(self)!""" + self._rox_lib_tasks[task] = True + +class IdleBlocker(Blocker): + """An IdleBlocker blocks until a task starts waiting on it, then + immediately triggers. An instance of this class is used internally + when a Task yields None.""" + def add_task(self, task): + """Also calls trigger.""" + Blocker.add_task(self, task) + self.trigger() + +class TimeoutBlocker(Blocker): + """Triggers after a set number of seconds. rox.toplevel_ref/unref + are called to prevent the app quitting while a TimeoutBlocker is + running.""" + def __init__(self, timeout): + """Trigger after 'timeout' seconds (may be a fraction).""" + Blocker.__init__(self) + rox.toplevel_ref() + g.timeout_add(long(timeout * 1000), self._timeout) + + def _timeout(self): + rox.toplevel_unref() + self.trigger() + +_idle_blocker = IdleBlocker() + +class Task: + """Create a new Task when you have some long running function to + run in the background, but which needs to do work in 'chunks'. + Example (the first line is needed to enable the 'yield' keyword in + python 2.2): + + from __future__ import generators + from rox import tasks + def my_task(start): + for x in range(start, start + 5): + print "x =", x + yield None + + tasks.Task(my_task(0)) + tasks.Task(my_task(10)) + + rox.mainloop() + + Yielding None gives up control of the processor to another Task, + causing the sequence printed to be interleaved. You can also yield a + Blocker (or a list of Blockers) if you want to wait for some + particular event before resuming (see the Blocker class for details). + """ + + def __init__(self, iterator, name = None): + """Call iterator.next() from a glib idle function. This function + can yield Blocker() objects to suspend processing while waiting + for events. name is used only for debugging.""" + assert iterator.next, "Object passed is not an iterator!" + self.next = iterator.next + self.name = name + # Block new task on the idle handler... + _idle_blocker.add_task(self) + self._rox_blockers = (_idle_blocker,) + + def _resume(self): + # Remove from our blockers' queues + for blocker in self._rox_blockers: + del blocker._rox_lib_tasks[self] + # Resume the task + try: + new_blockers = self.next() + except StopIteration: + # Task ended + return + except Exception: + # Task crashed + rox.report_exception() + return + if new_blockers is None: + # Just give up control briefly + new_blockers = (_idle_blocker,) + else: + if isinstance(new_blockers, Blocker): + # Wrap a single yielded blocker into a list + new_blockers = (new_blockers,) + # Are we blocking on something that already happened? + for blocker in new_blockers: + if blocker.happened: + new_blockers = (_idle_blocker,) + break + # Add to new blockers' queues + for blocker in new_blockers: + blocker.add_task(self) + self._rox_blockers = new_blockers + + def __repr__(self): + if self.name is None: + return "[Task]" + return "[Task '%s']" % self.name + +# Must append to _run_queue right after calling this! +def _schedule(): + assert not _run_queue + rox.toplevel_ref() + g.idle_add(_handle_run_queue) + +def _handle_run_queue(): + global _idle_blocker + assert _run_queue + + next = _run_queue[0] + assert next.happened + + if next is _idle_blocker: + # Since this blocker will never run again, create a + # new one for future idling. + _idle_blocker = IdleBlocker() + + tasks = next._rox_lib_tasks.keys() + #print "Resume", tasks + for task in tasks: + # Run 'task'. + task._resume() + + del _run_queue[0] + + if _run_queue: + return True + rox.toplevel_unref() + return False -- 2.11.4.GIT