def test_execute(self): with mock.patch('anki.storage.Collection') as anki_storage_Collection: col = anki_storage_Collection.return_value func = MagicMock() func.return_value = sentinel.some_object # check that execute works and auto-creates the collection wrapper = CollectionWrapper(self.collection_path) ret = wrapper.execute(func, [1, 2, 3], {'key': 'aoeu'}) self.assertEqual(ret, sentinel.some_object) anki_storage_Collection.assert_called_with(self.collection_path) func.assert_called_with(col, 1, 2, 3, key='aoeu') # check that execute always returns False if waitForReturn=False func.reset_mock() ret = wrapper.execute(func, [1, 2, 3], {'key': 'aoeu'}, waitForReturn=False) self.assertEqual(ret, None) func.assert_called_with(col, 1, 2, 3, key='aoeu')
class ThreadingCollectionWrapper(object): """Provides the same interface as CollectionWrapper, but it creates a new Thread to interact with the collection.""" def __init__(self, path, setup_new_collection=None): self.path = path self.wrapper = CollectionWrapper(path, setup_new_collection) self._queue = Queue() self._thread = None self._running = False self.last_timestamp = time.time() self.start() @property def running(self): return self._running def qempty(self): return self._queue.empty() def current(self): from threading import current_thread return current_thread() == self._thread def execute(self, func, args=[], kw={}, waitForReturn=True): """ Executes a given function on this thread with the *args and **kw. If 'waitForReturn' is True, then it will block until the function has executed and return its return value. If False, it will return None immediately and the function will be executed sometime later. """ if waitForReturn: return_queue = Queue() else: return_queue = None self._queue.put((func, args, kw, return_queue)) if return_queue is not None: ret = return_queue.get(True) if isinstance(ret, Exception): raise ret return ret def _run(self): logging.info('CollectionThread[%s]: Starting...', self.path) try: while self._running: func, args, kw, return_queue = self._queue.get(True) if hasattr(func, 'func_name'): func_name = func.func_name else: func_name = func.__class__.__name__ logging.info('CollectionThread[%s]: Running %s(*%s, **%s)', self.path, func_name, repr(args), repr(kw)) self.last_timestamp = time.time() try: ret = self.wrapper.execute(func, args, kw, return_queue) except Exception as e: logging.error( 'CollectionThread[%s]: Unable to %s(*%s, **%s): %s', self.path, func_name, repr(args), repr(kw), e, exc_info=True) # we return the Exception which will be raise'd on the other end ret = e if return_queue is not None: return_queue.put(ret) except Exception as e: logging.error( 'CollectionThread[%s]: Thread crashed! Exception: %s', self.path, e, exc_info=True) finally: self.wrapper.close() # clean out old thread object self._thread = None # in case we got here via an exception self._running = False logging.info('CollectionThread[%s]: Stopped!', self.path) def start(self): if not self._running: self._running = True assert self._thread is None self._thread = Thread(target=self._run) self._thread.start() def stop(self): def _stop(col): self._running = False self.execute(_stop, waitForReturn=False) def stop_and_wait(self): """ Tell the thread to stop and wait for it to happen. """ self.stop() if self._thread is not None: self._thread.join() # # Mimic the CollectionWrapper interface # def open(self): """Non-op. The collection will be opened on demand.""" pass def close(self): """Closes the underlying collection without stopping the thread.""" def _close(col): self.wrapper.close() self.execute(_close, waitForReturn=False) def opened(self): return self.wrapper.opened()