Exemplo n.º 1
0
class Task(object):
    def __init__(self, request):
        self._request = request
        self._cond = QWaitCondition()

    def getRequest(self):
        return self._request

    def waitForResponse(self):
        self._cond.wait(QMutex())
        x = self._response
        del self._response
        return x

    def addResponse(self, response):
        # Don't allow two responses to the same
        # request. Could possibly happen
        # if someone clicks accept/reject and simultaneously
        # closes the window, somehow, triggering
        # the closeEvent
        if self._cond:
            self._response = response
            self._cond.wakeAll()
            self._cond = None

    def setType(self, dialogType):
        """ Set what type of task this is. The dialogtype is 
        the construtor for the Dialog that is to be invoked, 
        when onSystrayActivated is invoked later on"""

        self.dialogType = dialogType

    def getType(self):
        return self.dialogType
Exemplo n.º 2
0
class VcsStatusMonitorThread(QThread):
    """
    Class implementing the VCS status monitor thread base class.
    
    @signal vcsStatusMonitorData(QStringList) emitted to update the VCS status
    @signal vcsStatusMonitorStatus(QString, QString) emitted to signal the status of the
        monitoring thread (ok, nok, op) and a status message
    """
    def __init__(self, interval, projectDir, vcs, parent = None):
        """
        Constructor
        
        @param interval new interval in seconds (integer)
        @param projectDir project directory to monitor (string or QString)
        @param vcs reference to the version control object
        @param parent reference to the parent object (QObject)
        """
        QThread.__init__(self, parent)
        self.setObjectName("VcsStatusMonitorThread")
        
        self.setTerminationEnabled(True)
        
        self.projectDir = QString(projectDir)
        self.vcs = vcs
        
        self.interval = interval
        self.autoUpdate = False
        
        self.statusList = QStringList()
        self.reportedStates = {}
        self.shouldUpdate = False
        
        self.monitorMutex = QMutex()
        self.monitorCondition = QWaitCondition()
        self.__stopIt = False
    
    def run(self):
        """
        Protected method implementing the tasks action.
        """
        while not self.__stopIt:
            # perform the checking task
            self.statusList.clear()
            self.emit(SIGNAL("vcsStatusMonitorStatus(QString, QString)"), 
                      QString("wait"), self.trUtf8("Waiting for lock"))
            try:
                locked = self.vcs.vcsExecutionMutex.tryLock(5000)
            except TypeError:
                locked = self.vcs.vcsExecutionMutex.tryLock()
            if locked:
                try:
                    self.emit(SIGNAL("vcsStatusMonitorStatus(QString, QString)"), 
                              QString("op"), self.trUtf8("Checking repository status"))
                    res, statusMsg = self._performMonitor()
                finally:
                    self.vcs.vcsExecutionMutex.unlock()
                if res:
                    status = QString("ok")
                else:
                    status = QString("nok")
                self.emit(SIGNAL("vcsStatusMonitorStatus(QString, QString)"), 
                          QString("send"), self.trUtf8("Sending data"))
                self.emit(SIGNAL("vcsStatusMonitorData(QStringList)"), 
                          QStringList(self.statusList))
                self.emit(SIGNAL("vcsStatusMonitorStatus(QString, QString)"), 
                          status, statusMsg)
            else:
                self.emit(SIGNAL("vcsStatusMonitorStatus(QString, QString)"), 
                          QString("timeout"), self.trUtf8("Timed out waiting for lock"))
            
            if self.autoUpdate and self.shouldUpdate:
                try:
                    self.vcs.vcsUpdate(self.projectDir, True)
                    continue    # check again
                except TypeError:
                    pass    # compatibility for older VCS plugins
                self.shouldUpdate = False
            
            # wait until interval has expired checking for a stop condition
            self.monitorMutex.lock()
            if not self.__stopIt:
                self.monitorCondition.wait(self.monitorMutex, self.interval * 1000)
            self.monitorMutex.unlock()
        
        self.exit()
    
    def setInterval(self, interval):
        """
        Public method to change the monitor interval.
        
        @param interval new interval in seconds (integer)
        """
        locked = self.monitorMutex.tryLock()
        self.interval = interval
        self.monitorCondition.wakeAll()
        if locked:
            self.monitorMutex.unlock()
    
    def getInterval(self):
        """
        Public method to get the monitor interval.
        
        @return interval in seconds (integer)
        """
        return self.interval
    
    def setAutoUpdate(self, auto):
        """
        Public method to enable the auto update function.
        
        @param auto status of the auto update function (boolean)
        """
        self.autoUpdate = auto
    
    def getAutoUpdate(self):
        """
        Public method to retrieve the status of the auto update function.
        
        @return status of the auto update function (boolean)
        """
        return self.autoUpdate
    
    def checkStatus(self):
        """
        Public method to wake up the status monitor thread.
        """
        locked = self.monitorMutex.tryLock()
        self.monitorCondition.wakeAll()
        if locked:
            self.monitorMutex.unlock()
    
    def stop(self):
        """
        Public method to stop the monitor thread.
        """
        locked = self.monitorMutex.tryLock()
        self.__stopIt = True
        self.monitorCondition.wakeAll()
        if locked:
            self.monitorMutex.unlock()

    def clearCachedState(self, name):
        """
        Public method to clear the cached VCS state of a file/directory.
        
        @param name name of the entry to be cleared (QString or string)
        """
        project = e4App().getObject("Project")
        key = project.getRelativePath(unicode(name))
        try:
            del self.reportedStates[key]
        except KeyError:
            pass

    def _performMonitor(self):
        """
        Protected method implementing the real monitoring action.
        
        This method must be overridden and populate the statusList member variable
        with a list of strings giving the status in the first column and the
        path relative to the project directory starting with the third column.
        The allowed status flags are:
        <ul>
            <li>"A" path was added but not yet comitted</li>
            <li>"M" path has local changes</li>
            <li>"O" path was removed</li>
            <li>"R" path was deleted and then re-added</li>
            <li>"U" path needs an update</li>
            <li>"Z" path contains a conflict</li>
            <li>" " path is back at normal</li>
        </ul>
        
        @return tuple of flag indicating successful operation (boolean) and 
            a status message in case of non successful operation (QString)
        """
        raise RuntimeError('Not implemented')
Exemplo n.º 3
0
class _FTSearchThread(QThread):
    '''This thread performs full text search in the background'''

    searchFinished = pyqtSignal()
    searchError = pyqtSignal()

    def __init__(self, searcher, parent):
        QThread.__init__(self, parent)
        self._searcher = searcher
        self._quit = False
        self._mutex = QMutex()
        self._pending = QWaitCondition()
        self._collector = None
        self._query = None
        self._result = None

    def run(self):
        while not self._quit:
            self._mutex.lock()
            if not self._query:
                self._pending.wait(self._mutex)
            query = self._query
            self._query = None
            self._mutex.unlock()

            # search
            if query:
                (query_str1, query_str2, itemtypes, limit, highlight,
                 merge) = query

                self._mutex.lock()
                collector = self._searcher.make_collector(limit)
                self._collector = collector
                self._mutex.unlock()

                try:
                    result = self._searcher.search(collector, query_str1,
                                                   query_str2, itemtypes,
                                                   highlight)
                except:
                    self._mutex.lock()
                    self._result = None
                    self._mutex.unlock()
                    self.searchError.emit()
                else:
                    if collector.aborted:
                        pass
                    else:
                        self._mutex.lock()
                        self._result = (merge, result)
                        self._mutex.unlock()
                        self.searchFinished.emit()

                self._mutex.lock()
                self._collector = None
                self._mutex.unlock()

    def cancel(self):
        self._mutex.lock()
        self._query = None
        if self._collector:
            self._collector.abort()
        self._mutex.unlock()

    def quit(self):
        self._mutex.lock()
        if self._collector:
            self._collector.abort()
        self._query = None
        self._quit = True
        self._mutex.unlock()
        self._pending.wakeAll()

    def update_query(self,
                     query_str1=None,
                     query_str2=None,
                     itemtypes=(),
                     limit=1000,
                     highlight=False,
                     merge=False):
        self._mutex.lock()
        if self._collector:
            self._collector.abort()
        self._query = (query_str1, query_str2, itemtypes, limit, highlight,
                       merge)
        self._mutex.unlock()
        self._pending.wakeAll()

    def take_result(self):
        self._mutex.lock()
        r = self._result
        self._result = None
        self._mutex.unlock()
        return r
Exemplo n.º 4
0
class TestThread(QThread):
    
    def __init__(self, suite, items, stopOnError, stopOnFail, xmlRPCUrl):
        QThread.__init__(self)
        self.suite = suite
        self.items = items
        executionDelay = self.suite.sliderSpeed.value() / 1000 # convert to seconds
        self.sync = QMutex()
        self.pauseCond = QWaitCondition()
        self.executionPaused = False  
        self.autoPlayer = AutoPlayer(executionDelay, self.sync, self.pauseCond, xmlRPCUrl)
        self.executionStopped = False         
        self.stopOnError = stopOnError
        self.stopOnFail = stopOnFail
        self._connectSignals()
               
    def _connectSignals(self):
        self.connect(self.suite,SIGNAL("pauseExecution"), self.pauseExecution)
        self.connect(self.suite,SIGNAL("unpauseExecution"), self.unpauseExecution)
        self.connect(self.suite,SIGNAL("executionStopped"), self.stopExecution)
        self.connect(self.autoPlayer,SIGNAL("logMessage"), self.suite.logMessage)
        self.connect(self.suite.sliderSpeed, SIGNAL('valueChanged(int)'), self.updateExecutionSpeed)
        
    def checkForPause(self):
        """ Check if the pause flag has been set"""
        self.sync.lock()
        if(self.executionPaused):
            self.pauseCond.wait(self.sync)
        self.sync.unlock()

    def run(self):
        self.emit(SIGNAL('updateExecutionButtons'), False)
        # set default icon for all items
        for item in self.items:
            self.emit(SIGNAL('updateItemStatus'), item, TestCaseStatus.UNTESTED, False)
        
        # parse and execute test caeses 
        handNumber = 1  
        failedTc = 0
        successfulTc = 0
        errorTc = 0 
        for item in self.items:     
            try:
                
                self.checkForPause()    
                if self.executionStopped:
                    return
                
                self.emit(SIGNAL('logMessage'), " => Test case : " + item.text(),LogStyle.TITLE)
                self.emit(SIGNAL('updateItemStatus'), item, TestCaseStatus.UNTESTED, True)
                tc = item.data(Qt.UserRole) # get file name
                
                #parse test case 
                tcp = TestCaseParser(tc.file)
                tcp.parse()
                
                #start test
                successful = self.autoPlayer.startTest(tcp, handNumber)
                
                self.checkForPause()
                if self.executionStopped:
                    return
                
                if successful:
                    self.emit(SIGNAL('updateItemStatus'), item, TestCaseStatus.SUCCESS, True)
                    successfulTc += 1
                else:
                    self.emit(SIGNAL('updateItemStatus'), item, TestCaseStatus.FAILED, True)
                    failedTc += 1
                    if(self.stopOnFail):
                        break
                handNumber += 1
            except ParserException as e:
                self.emit(SIGNAL('updateItemStatus'), item, TestCaseStatus.ERROR, True)
                self.emit(SIGNAL('logMessage'),"Parsing error: " + str(e),LogStyle.WARNING)
                errorTc += 1
                if(self.stopOnError):
                    break
            except socket.error:
                self.emit(SIGNAL('logMessage'),"Can't connect to player",LogStyle.ERROR)
                self.stopExecution()
                self.emit(SIGNAL('updateExecutionButtons'), True) 
                return
            except Exception as e:
                self.emit(SIGNAL('logMessage'),"Unknown error: " + str(e),LogStyle.ERROR)
                raise         
        
        self.emit(SIGNAL('logMessage'), " => Execution finished",LogStyle.TITLE)
        if(len(self.items) > 1):                 
            self.emit(SIGNAL('logMessage'), "Result => Total: {0}, Successful: {1}, Failed: {2}, Error: {3}"
                      .format(len(self.items),successfulTc,failedTc,errorTc),LogStyle.TITLE)
                
        self.emit(SIGNAL('updateExecutionButtons'), True) 
        
    def pauseExecution(self):
        """ Pause test case execution """
        self.sync.lock()
        self.executionPaused = True
        self.autoPlayer.pause()
        self.sync.unlock()
      
    def unpauseExecution(self):
        """ Unpause test case execution """
        self.sync.lock()
        self.executionPaused = False
        self.autoPlayer.unpause()
        self.pauseCond.wakeAll()
        self.sync.unlock()
        
    def stopExecution(self):   
        """ Stop test case execution """ 
        if(self.executionPaused):
            self.unpauseExecution()
        self.executionStopped = True
        self.autoPlayer.stop()
        
    def updateExecutionSpeed(self, value):
        executionDelay = value / 1000 # convert to seconds
        self.autoPlayer.executionDelay = executionDelay
Exemplo n.º 5
0
class RenderThread(QThread):
    """
    Thread object that takes control over an actual object exposing thoses
    methods with immediate return with delayed responses.
    """
    def __init__(self, parent):
        QThread.__init__(self, parent)

        self.classObjectLock = QMutex(QMutex.Recursive)
        self.classParamDict = {}
        self.classInstanceDict = {}

        self.queueLock = QMutex(QMutex.Recursive)
        self.queueHasJobsLock = QMutex()      # lock lighter than queueLock
                                              #   while waiting for this lock no
                                              #   lock on the latter can be hold
        self.queueHasJobsCondition = QWaitCondition()
        self.renderQueue = []                 # contains all render requests
        self.newestId = 0                     # newest job Id

        self.renderingLock = QMutex(QMutex.Recursive)
        self.renderingFinishedLock = QMutex() # lock lighter than renderingLock
                                              #   while waiting for this lock no
                                              #   lock on the latter can be hold
        self.renderingFinishedCondition = QWaitCondition()
        self.currentlyRenderingJob = None

    def quit(self):
        self.dequeueAll()

        while self.classInstanceDict:
            classObject = self.classInstanceDict.keys()[0]
            self.removeObject(classObject)

        # enqueue meta command
        self.enqueue(None, 'quit')
        self.wait()

    def setObject(self, classObject, *args, **param):
        """Add or reset a class handled by this render thread."""
        if not self.isRunning():
            raise Exception(
                "Thread needs to be running before objects can be created.")

        self.classObjectLock.lock()
        if classObject in self.classParamDict:
            # TODO remove, or change reloadObject()
            #if self.classParamDict[classObject] == (args, param):
                #self.classObjectLock.unlock()
                ## no changes done
                #return

            self.removeObject(classObject)

        self.classParamDict[classObject] = (args, param)
        self.classInstanceDict[classObject] = None

        self.enqueueWait(classObject, '__init__', *args, **param)
        #self.enqueue(classObject, '__init__', *args, **param)

        ## return only once object is created
        #self.renderingFinishedLock.lock()
        #while not self.classInstanceDict[classObject]:
            #self.renderingFinishedCondition.wait(self.renderingFinishedLock)
        #self.renderingFinishedLock.unlock()
        #self.classObjectLock.unlock()

    def reloadObject(self, classObject):
        """Reloads a class object."""
        self.classObjectLock.lock()
        if classObject not in self.classParamDict:
            self.classObjectLock.unlock()
            raise Exception("Object not set")

        args, param = self.classParamDict[classObject]
        self.setObject(classObject, *args, **param)
        self.classObjectLock.unlock()

    def hasObject(self, classObject):
        """returns the object's instance created by the thread."""
        QMutexLocker(self.classObjectLock)
        return classObject in self.classInstanceDict

    def getObjectInstance(self, classObject):
        """returns the object's instance created by the thread."""
        QMutexLocker(self.classObjectLock)
        return self.classInstanceDict[classObject]

    def removeObject(self, classObject):
        """
        Removes the given object's instance from the render thread, removing all
        affiliated jobs from the queue and canceling an eventually current
        rendered method.
        """
        self.classObjectLock.lock()
        self.queueHasJobsLock.lock()
        self.queueLock.lock()
        # clear all not yet rendered content from the queue
        newQueue = []
        for entry in self.renderQueue:
            jobId, entryClassObject, _, _, _ = entry
            if classObject != entryClassObject:
                newQueue.append(entry)
            else:
                self.emit(SIGNAL("jobDequeued"), jobId)

        self.renderQueue = newQueue

        # interrupt currently rendering
        self.renderingLock.lock()
        if self.currentlyRenderingJob:
            _, entryClassObject, _, _, _ = self.currentlyRenderingJob
            if classObject == entryClassObject:
                self.cancelCurrentJob()
        self.renderingLock.unlock()

        del self.classParamDict[classObject]
        del self.classInstanceDict[classObject]

        self.queueHasJobsLock.unlock()
        self.queueLock.unlock()
        self.classObjectLock.unlock()

    def enqueue(self, classObject, method, *args, **param):
        self.classObjectLock.lock()
        if classObject and classObject not in self.classParamDict:
            self.classObjectLock.unlock()
            raise Exception("Object not set")

        self.queueHasJobsLock.lock()
        self.queueLock.lock()

        self.newestId = (self.newestId + 1) % sys.maxint

        jobId = self.newestId
        self.renderQueue.append((jobId, classObject, method, args, param))

        self.queueLock.unlock()

        self.queueHasJobsCondition.wakeAll()
        self.queueHasJobsLock.unlock()
        self.classObjectLock.unlock()

        if classObject:
            self.emit(SIGNAL("jobEnqueued"), jobId)
            return jobId

    def enqueueWait(self, classObject, method, *args, **param):
        """Enqueues a job and waits until it finishes."""
        def inQueue(jobId):
            QMutexLocker(self.queueLock)
            for qjobId, _, _, _, _ in self.renderQueue:
                if jobId == qjobId:
                    return True

            QMutexLocker(self.renderingLock)
            if self.currentlyRenderingJob:
                qjobId, _, _, _, _ = self.currentlyRenderingJob
                if jobId == qjobId:
                    return True
            return False

        jobId = self.enqueue(classObject, method, *args, **param)
        if jobId == None:
            return

        # return only once method has finished
        self.renderingFinishedLock.lock()
        while inQueue(jobId):
            self.renderingFinishedCondition.wait(self.renderingFinishedLock)
        self.renderingFinishedLock.unlock()
        return jobId

    def getJobEntry(self, jobId):
        self.queueLock.lock()
        for idx, entry in enumerate(self.renderQueue):
            entryJobId, _, _, _, _ = entry
            if entryJobId == jobId:
                break
        else:
            entryJobId = None

        self.queueLock.unlock()
        return entryJobId

    def dequeue(self, jobId):
        QMutexLocker(self.queueHasJobsLock)
        QMutexLocker(self.queueLock)
        # search for entry
        jobEntry = self.getJobEntry(jobId)
        if jobEntry:
            idx = self.renderQueue.index(jobEntry)
            del self.renderQueue[idx]
            self.emit(SIGNAL("jobDequeued"), jobId)

            return True
        else:
            # interrupt currently rendering
            QMutexLocker(self.renderingLock)
            if self.currentlyRenderingJob:
                entryJobId, _, _, _, _ = self.currentlyRenderingJob
                if entryJobId == jobId:
                    self.cancelCurrentJob()
                    return True

            return False

    def dequeueMethod(self, classObject, method):
        self.queueHasJobsLock.lock()
        self.queueLock.lock()
        # clear all not yet rendered content from the queue
        newQueue = []

        for entry in self.renderQueue:
            jobId, entryClassObject, entryMethod, _, _ = entry
            if classObject != entryClassObject or method != entryMethod:
                newQueue.append(entry)
            else:
                self.emit(SIGNAL("jobDequeued"), jobId)

        self.renderQueue = newQueue

        # interrupt currently rendering
        self.renderingLock.lock()
        if self.currentlyRenderingJob:
            _, entryClassObject, entryMethod, _, _ = self.currentlyRenderingJob
            if classObject == entryClassObject and method == entryMethod:
                self.cancelCurrentJob()
        self.renderingLock.unlock()

        self.queueLock.unlock()
        self.queueHasJobsLock.unlock()

    def dequeueAll(self):
        self.queueHasJobsLock.lock()
        self.queueLock.lock()
        # signal all
        for jobId, _, _, _, _ in self.renderQueue:
            self.emit(SIGNAL("jobDequeued"), jobId)

        self.renderQueue = []

        # interrupt currently rendering
        self.renderingLock.lock()
        if self.currentlyRenderingJob:
            self.cancelCurrentJob()

        self.renderingLock.unlock()
        self.queueLock.unlock()
        self.queueHasJobsLock.unlock()

    def isRendering(self):
        self.queueLock.lock()
        self.renderingLock.lock()
        rendering = len(self.renderQueue) > 0 \
            or self.currentlyRenderingJob != None
        self.renderingLock.unlock()
        self.queueLock.unlock()
        return rendering

    def cancelCurrentJob(self):
        """
        This method is called when the currently rendered job should be
        canceled. The default implementation doesn't handle any object specific
        cancel operations and by default returns False. An actual implementation
        should call the current object's cancel routine and then call
        clearCurrentJob() to clear the current job and finally return True.
        """
        return False

    def clearCurrentJob(self):
        """
        This method needs to be called before the current job is canceled.
        """
        self.renderingLock.lock()
        self.currentlyRenderingJob = None
        self.renderingLock.unlock()

    def run(self):
        while True:
            self.queueHasJobsLock.lock()

            hasJobWaiting = len(self.renderQueue) > 0
            while not hasJobWaiting:
                self.emit(SIGNAL("queueEmpty"))
                self.queueHasJobsCondition.wait(self.queueHasJobsLock)
                hasJobWaiting = len(self.renderQueue) > 0

            self.queueLock.lock()
            self.renderingLock.lock()
            self.currentlyRenderingJob = self.renderQueue.pop(0)
            jobId, entryClassObject, method, args, param \
                = self.currentlyRenderingJob

            self.renderingLock.unlock()
            self.queueLock.unlock()
            self.queueHasJobsLock.unlock()

            if entryClassObject != None:
                if method == '__init__':
                    classInstance = entryClassObject(*args, **param)
                    self.classInstanceDict[entryClassObject] = classInstance

                    self.emit(SIGNAL("objectCreated"), jobId, entryClassObject)
                else:
                    classInstance = self.classInstanceDict[entryClassObject]
                    try:
                        content = getattr(classInstance, method)(*args, **param)
                        self.finishJob(jobId, entryClassObject, method, args,
                            param, content)
                    except BaseException, e:
                        if self.currentlyRenderingJob:
                            stacktrace = traceback.format_exc()
                            self.emit(SIGNAL("jobErrorneous"), jobId,
                                entryClassObject, method, args, param, e,
                                stacktrace)
                        else:
                            # job got canceled
                            self.emit(SIGNAL("jobCanceled"), jobId,
                                entryClassObject, method, args, param)

            self.renderingFinishedLock.lock()
            self.clearCurrentJob()
            self.renderingFinishedCondition.wakeAll()
            self.renderingFinishedLock.unlock()

            if entryClassObject == None and method == 'quit':
                return
Exemplo n.º 6
0
class _FTSearchThread(QThread):
    '''This thread performs full text search in the background'''

    searchFinished = pyqtSignal()
    searchError = pyqtSignal()

    def __init__(self, searcher, parent):
        QThread.__init__(self, parent)
        self._searcher = searcher
        self._quit = False
        self._mutex = QMutex()
        self._pending = QWaitCondition()
        self._collector = None
        self._query = None
        self._result = None

    def run(self):
        while not self._quit:
            self._mutex.lock()
            if not self._query:
                self._pending.wait(self._mutex)
            query = self._query
            self._query = None
            self._mutex.unlock()

            # search
            if query:
                (query_str1, query_str2, itemtypes,
                 limit, highlight, merge) = query

                self._mutex.lock()
                collector = self._searcher.make_collector(limit)
                self._collector = collector
                self._mutex.unlock()

                try:
                    result = self._searcher.search(
                        collector,
                        query_str1, query_str2,
                        itemtypes, highlight)
                except:
                    self._mutex.lock()
                    self._result = None
                    self._mutex.unlock()
                    self.searchError.emit()
                else:
                    if collector.aborted:
                        pass
                    else:
                        self._mutex.lock()
                        self._result = (merge, result)
                        self._mutex.unlock()
                        self.searchFinished.emit()

                self._mutex.lock()
                self._collector = None
                self._mutex.unlock()

    def cancel(self):
        self._mutex.lock()
        self._query = None
        if self._collector:
            self._collector.abort()
        self._mutex.unlock()

    def quit(self):
        self._mutex.lock()
        if self._collector:
            self._collector.abort()
        self._query = None
        self._quit = True
        self._mutex.unlock()
        self._pending.wakeAll()

    def update_query(self, query_str1=None, query_str2=None, itemtypes=(),
                     limit=1000, highlight=False, merge=False):
        self._mutex.lock()
        if self._collector:
            self._collector.abort()
        self._query = (query_str1, query_str2,
                       itemtypes, limit, highlight, merge)
        self._mutex.unlock()
        self._pending.wakeAll()

    def take_result(self):
        self._mutex.lock()
        r = self._result
        self._result = None
        self._mutex.unlock()
        return r