def run(self, runstate=None): print("run...") if runstate is None: runstate = RunState(str(self)) # Display and interact with the Window using an Event Loop window = self.window print("window =", window) with runstate: while not runstate.cancelled: print("event?") event, values = window.read() print("event =", repr(event), repr(values)) # See if user wants to quit or window was closed if event == sg.WINDOW_CLOSED or event == 'Quit': runstate.cancel() elif event == self.tree.key: record_key, = values[event] print("record_key =", record_key) try: record = self.tree[record_key] except KeyError as e: warning("no self.tree[%r]: %s", record_key, e) else: print("record =", record) self.fspath = record.fullpath else: warning("unexpected event %r: %r", event, values)
class BlockCache: ''' A temporary file based cache for whole Blocks. This is to support filesystems' and files' direct read/write actions by passing them straight through to this cache is there's a mapping. We accrue complete Block contents in unlinked files. ''' # default cace size MAX_FILES = 32 MAX_FILE_SIZE = 1024 * 1024 * 1024 def __init__(self, tmpdir=None, suffix='.dat', max_files=None, max_file_size=None): if max_files is None: max_files = self.MAX_FILES if max_file_size is None: max_file_size = self.MAX_FILE_SIZE self.tmpdir = tmpdir self.suffix = suffix self.max_files = max_files self.max_file_size = max_file_size self.blockmaps = {} # hashcode -> BlockMapping self._tempfiles = [] # in play BlockTempfiles self._lock = Lock() self.runstate = RunState() self.runstate.start() def close(self): ''' Release all the blockmaps. ''' self.runstate.cancel() with self._lock: self.blockmaps = {} for tempf in self._tempfiles: tempf.close() self._tempfiles = [] def __getitem__(self, hashcode): ''' Fetch BlockMapping associated with `hashcode`, raise KeyError if missing. ''' return self.blockmaps[hashcode] def get_blockmap(self, block): ''' Add the specified Block to the cache, return the BlockMapping. ''' blockmaps = self.blockmaps h = block.hashcode with self._lock: bm = blockmaps.get(h) if bm is None: tempfiles = self._tempfiles if tempfiles: tempf = tempfiles[-1] if tempf.size >= self.max_file_size: tempf = None else: tempf = None if tempf is None: while len(tempfiles) >= self.max_files: otempf = tempfiles.pop(0) otempf.close() tempf = BlockTempfile(self, self.tmpdir, self.suffix) tempfiles.append(tempf) bm = tempf.append_block(block, self.runstate) blockmaps[h] = bm return bm
class Task(Result): ''' A task which may require the completion of other tasks. This is a subclass of `Result`. Keyword parameters: * `cancel_on_exception`: if true, cancel this `Task` if `.call` raises an exception; the default is `False`, allowing repair and retry * `cancel_on_result`: optional callable to test the `Task.result` after `.call`; if it returns `True` the `Task` is marked as cancelled * `func`: the function to call to complete the `Task`; it will be called as `func(*func_args,**func_kwargs)` * `func_args`: optional positional arguments, default `()` * `func_kwargs`: optional keyword arguments, default `{}` * `lock`: optional lock, default an `RLock` Other arguments are passed to the `Result` initialiser. Example: t1 = Task(name="task1") t1.bg(time.sleep, 10) t2 = Task("name="task2") # prevent t2 from running until t1 completes t2.require(t1) # try to run sleep(5) for t2 immediately after t1 completes t1.notify(t2.call, sleep, 5) The model here may not be quite as expected; it is aimed at tasks which can be repaired and rerun. As such, if `self.call(func,...)` raises an exception from `func` then this `Task` will still block dependent `Task`s. Dually, a `Task` which completes without an exception is considered complete and does not block dependent `Task`s. To cancel dependent `Tasks` the function should raise a `CancellationError`. Users wanting more immediate semantics can supply `cancel_on_exception` and/or `cancel_on_result` to control these behaviours. Example: t1 = Task(name="task1") t1.bg(time.sleep, 2) t2 = Task("name="task2") # prevent t2 from running until t1 completes t2.require(t1) # try to run sleep(5) for t2 immediately after t1 completes t1.notify(t2.call, sleep, 5) >>> ''' _seq = Seq() _state = ThreadState(current_task=None) def __init__(self, *a, lock=None, cancel_on_exception=False, cancel_on_result=None, func, func_args=(), func_kwargs=None, **kw): if lock is None: lock = RLock() if func_kwargs is None: func_kwargs = {} super().__init__(*a, lock=lock, **kw) self._required = set() self.cancel_on_exception = cancel_on_exception self.cancel_on_result = cancel_on_result self.func = func self.func_args = func_args self.func_kwargs = func_kwargs self.runstate = RunState(self.name) def __hash__(self): return id(self) def __eq__(self, otask): return self is otask @classmethod def current_task(cls): ''' The current `Task`, valid during `Task.call()`. This allows the function called by the `Task` to access the task, typically to poll its `.runstate` attribute. ''' return cls._state.current_task # pylint: disable=no-member def abort(self): ''' Calling `abort()` calls `self.runstate.cancel()` to indicate to the running function that it should cease operation. ''' self.runstate.cancel() def required(self): ''' Return a `set` containing any required tasks. ''' with self._lock: return set(self._required) def require(self, otask): ''' Add a requirement that `otask` be complete before we proceed. ''' assert otask is not self assert self.state == ResultState.pending with self._lock: self._required.add(otask) def block(self, otask): ''' Block another task until we are complete. ''' otask.require(self) def then(self, func, *a, **kw): ''' Queue a call to `func(*a,**kw)` to run after the completion of this task. This supports a chain of actions: >>> t = Task(func=lambda: 1) >>> final_t = t.then(print,1).then(print,2) >>> final_t.ready # the final task has not yet run False >>> # finalise t, wait for final_t (which runs immediately) >>> t.call(); print(final_t.join()) 1 2 (None, None) >>> final_t.ready True ''' post_task = type(self)(func=func, func_args=a, func_kwargs=kw) post_task.require(self) self.notify(lambda _: post_task.bg()) return post_task def blockers(self): ''' A generator yielding tasks from `self.required()` which should block this task. Cancelled tasks are not blockers but if we encounter one we do cancel the current task. ''' for otask in self.required(): if otask.cancelled: warning("%s cancelled because %s is also cancelled" % (self, otask)) self.cancel() continue if not otask.ready: yield otask continue if otask.exc_info: yield otask continue # pylint: disable=arguments-differ def bg(self): ''' Submit a function to complete the `Task` in a separate `Thread`, returning the `Thread`. This dispatches a `Thread` to run `self.call()` and as such the `Task` must be in "pending" state, and transitions to "running". ''' return bg_thread(self.call, name=self.name) # pylint: disable=arguments-differ def call(self): ''' Attempt to perform the `Task` by calling `func(*func_args,**func_kwargs)`. If we are cancelled, raise `CancellationError`. If there are blocking required tasks, raise `BlockedError`. Otherwise run `r=func(self,*self.func_args,**self.func_kwargsw)` with the following effects: * if `func()` raises a `CancellationError`, cancel the `Task` * otherwise, if an exception is raised and `self.cancel_on_exception` is true, cancel the `Task`; store the exception information from `sys.exc_info()` as `self.exc_info` regardless * otherwise, if `self.cancel_on_result` is not `None` and `self.cancel_on_result(r)` is true, cancel the `Task`; store `r` as `self.result` regardless If we were cancelled, raise `CancellationError`. During the duration of the call the property `Task.current_task` is set to `self` allowing access to the `Task`. A typical use is to access the current `Task`'s `.runstate` attribute which can be polled by long running tasks to honour calls to `Task.abort()`. ''' if not self.cancelled: for otask in self.blockers(): raise BlockedError("%s blocked by %s" % (self, otask)) if not self.cancelled: state = type(self)._state with self._lock: with state(current_task=self): try: with self.runstate: r = self.func(*self.func_args, **self.func_kwargs) except CancellationError: self.cancel() except BaseException: if self.cancel_on_exception: self.cancel() # store the exception regardless self.exc_info = sys.exc_info() else: if self.cancel_on_result and self.cancel_on_result( r): self.cancel() # store the result regardless self.result = r if self.cancelled: raise CancellationError() def callif(self): ''' Trigger a call to `func(self,*self.func_args,**self.func_kwargsw)` if we're pending and not blocked or cancelled. ''' with self._lock: if not self.ready: try: self.call() except (BlockedError, CancellationError) as e: debug("%s.callif: %s", self, e)