def __init__(self, title): self._title = title self._subtitle = 'Queued' self.scheduling_style = SCHEDULING_STYLE_NONE self._children = [] self._num_children_complete = 0 self._complete = False self.listeners = [] self._did_yield_self = False # used by leaf tasks self._future = Future() # used by leaf tasks # TODO: Consider merging the following two fields self._first_incomplete_child_index = 0 # used by SCHEDULING_STYLE_SEQUENTIAL self._next_child_index = 0 # used by SCHEDULING_STYLE_ROUND_ROBIN
class Task(object): """ Encapsulates a long-running process that reports its status occasionally. A task may depend on the results of a child task during its execution. Generally there are two kinds of tasks: (1) Leaf tasks - Performs a single long-running operation on a background thread and completes immediately after this operation is complete. - The operation is executed by the __call__() method, which must be implemented by leaf task subclasses. (2) Container tasks - Uses child tasks to perform all its work. - Should set the 'scheduling_style' property in its constructor. - Should add the initial set of children in its constructor. - May add additional children tasks over time to perform additional work. - Generally this is done upon the completion of a child task. - Automatically listen to child tasks. A container task may override: o child_task_subtitle_did_change o child_task_did_complete Tasks must generally be manipulated on the foreground thread unless documented otherwise. """ def __init__(self, title): self._title = title self._subtitle = 'Queued' self.scheduling_style = SCHEDULING_STYLE_NONE self._children = [] self._num_children_complete = 0 self._complete = False self.listeners = [] self._did_yield_self = False # used by leaf tasks self._future = Future() # used by leaf tasks # TODO: Consider merging the following two fields self._first_incomplete_child_index = 0 # used by SCHEDULING_STYLE_SEQUENTIAL self._next_child_index = 0 # used by SCHEDULING_STYLE_ROUND_ROBIN # === Properties === @property def title(self): """ The title of this task. Fixed upon initialization. """ return self._title def _get_subtitle(self): """ The subtitle for this task. The setter (but not the getter) is threadsafe. """ return self._subtitle def _set_subtitle(self, value): def fg_task(): #print '%s -> %s' % (self, value) self._subtitle = value for lis in self.listeners: if hasattr(lis, 'task_subtitle_did_change'): lis.task_subtitle_did_change(self) fg_call_later(fg_task) subtitle = property(_get_subtitle, _set_subtitle) @property def children(self): return self._children @property def num_children_complete(self): return self._num_children_complete @property def complete(self): """ Whether this task is complete. """ return self._complete @property def future(self): """ Returns a Future that receives the result of this task. This property is only defined by default for leaf tasks. Container tasks may optionally override this if they conceptually return a value. """ if callable(self): return self._future else: raise ValueError('Container tasks do not define a result by default.') # === Protected Operations === def append_child(self, child): self._children.append(child) child.listeners.append(self) for lis in self.listeners: if hasattr(lis, 'task_did_append_child'): lis.task_did_append_child(self, child) def finish(self): """ Marks this task as completed. Threadsafe. """ def fg_task(): self._complete = True self.subtitle = 'Complete' # NOTE: Making a copy of the listener list since it is likely to be modified by callees. for lis in list(self.listeners): if hasattr(lis, 'task_did_complete'): lis.task_did_complete(self) fg_call_later(fg_task) def clear_children(self): """ Clears all of this task's children. Recommended only for use by RootTask. """ if not all(c.complete for c in self.children): raise ValueError('Some children are not complete.') self._children = [] self._first_incomplete_child_index = 0 self._next_child_index = 0 for lis in self.listeners: if hasattr(lis, 'task_did_clear_children'): lis.task_did_clear_children(self) # === Public Operations === def try_get_next_task_unit(self): """ Returns a callable ("task unit") that completes a unit of work for this task, or None if no more units can be provided until at least one of the previously returned units completes. Task units may be run on any thread. If this is a leaf task, its own __call__() method will be returned as the solitary task unit. As a task unit, it must be designed to run on any thread. """ if self.complete: return None if callable(self): if not self._did_yield_self: self._did_yield_self = True return self._call_self_and_record_result else: return None else: if len(self.children) == 0: raise ValueError('Container task has no children tasks.') if self.scheduling_style == SCHEDULING_STYLE_NONE: raise ValueError('Container task has not specified a scheduling style.') elif self.scheduling_style == SCHEDULING_STYLE_SEQUENTIAL: while self._first_incomplete_child_index < len(self.children): if self.children[self._first_incomplete_child_index].complete: self._first_incomplete_child_index += 1 else: cur_child_index = self._first_incomplete_child_index while cur_child_index < len(self.children): unit = self.children[cur_child_index].try_get_next_task_unit() if unit is not None: return unit cur_child_index += 1 return None return None elif self.scheduling_style == SCHEDULING_STYLE_ROUND_ROBIN: cur_child_index = self._next_child_index while True: unit = self.children[cur_child_index].try_get_next_task_unit() if unit is not None: self._next_child_index = (cur_child_index + 1) % len(self.children) return unit cur_child_index = (cur_child_index + 1) % len(self.children) if cur_child_index == self._next_child_index: # Wrapped around and back to where we started without finding anything to do return None else: raise ValueError('Container task has an unknown scheduling style (%s).' % self.scheduling_style) def _call_self_and_record_result(self): # (Ignore client requests to cancel) self._future.set_running_or_notify_cancel() try: self._future.set_result(self()) except: (_, e, _) = sys.exc_info() self._future.set_exception(e) finally: self.finish() # === Internal Events === def task_subtitle_did_change(self, task): if hasattr(self, 'child_task_subtitle_did_change'): self.child_task_subtitle_did_change(task) def task_did_complete(self, task): self._num_children_complete += 1 if hasattr(self, 'child_task_did_complete'): self.child_task_did_complete(task) task.listeners.remove(self)