AssistantEric/APIsManager.py

Sat, 01 Oct 2011 16:47:20 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 01 Oct 2011 16:47:20 +0200
changeset 40
f22c4a330026
parent 39
7b6ca9397ecc
child 50
005b6127b978
permissions
-rw-r--r--

Fixed an issue which would cause a traceback if the plug-in is used with eric 5.1.

# -*- coding: utf-8 -*-

# Copyright (c) 2008 - 2011 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the APIsManager.
"""

import os

from PyQt4.QtCore import QTimer, QThread, QFileInfo, pyqtSignal, QCoreApplication, \
     QEvent, QDateTime, QObject, Qt
from PyQt4.QtSql import QSqlDatabase, QSqlQuery

from E5Gui.E5Application import e5App

import QScintilla.Lexers

from DocumentationTools.APIGenerator import APIGenerator
import Utilities.ModuleParser
import Utilities
import Preferences

WorkerStarted = QEvent.User + 2001
WorkerFinished = QEvent.User + 2002
WorkerAborted = QEvent.User + 2003

ApisNameProject = "__Project__"


class DbAPIsWorker(QThread):
    """
    Class implementing a worker thread to prepare the API database.
    """
    populate_api_stmt = """
        INSERT INTO api (acWord, context, fullContext, signature, fileId, pictureId)
        VALUES (:acWord, :context, :fullContext, :signature, :fileId, :pictureId)
    """
    populate_del_api_stmt = """
        DELETE FROM api WHERE fileId = :fileId
    """
    populate_bases_stmt = """
        INSERT INTO bases (class, baseClasses, fileId)
        VALUES (:class, :baseClasses, :fileId)
    """
    populate_del_bases_stmt = """
        DELETE FROM bases WHERE fileId = :fileId
    """
    populate_file_stmt = """
        INSERT INTO file (file) VALUES (:file)
    """
    update_file_stmt = """
        UPDATE file SET lastRead = :lastRead WHERE file = :file
    """

    file_loaded_stmt = """
        SELECT lastRead from file WHERE file = :file
    """
    file_id_stmt = """
        SELECT id FROM file WHERE file = :file
    """
    file_delete_id_stmt = """
        DELETE FROM file WHERE id = :id
    """
    
    def __init__(self, proxy, language, apiFiles, projectPath="", refresh=False):
        """
        Constructor
        
        @param proxy reference to the object that is proxied (DbAPIs)
        @param language language of the APIs object (string)
        @param apiFiles list of API files to process (list of strings)
        @param projectPath path of the project. Only needed, if the APIs
            are extracted out of the sources of a project. (string)
        @param refresh flag indicating a refresh of the APIs of one file (boolean)
        """
        QThread.__init__(self)
        
        self.setTerminationEnabled(True)
        
        # Get the AC word separators for all of the languages that the editor supports.
        # This has to be before we create a new thread, because access to GUI elements
        # is not allowed from non-gui threads.
        self.__wseps = {}
        for lang in QScintilla.Lexers.getSupportedLanguages():
            lexer = QScintilla.Lexers.getLexer(lang)
            if lexer is not None:
                self.__wseps[lang] = lexer.autoCompletionWordSeparators()
        
        self.__proxy = proxy
        self.__language = language
        self.__apiFiles = apiFiles[:]
        self.__aborted = False
        self.__projectPath = projectPath
        self.__refresh = refresh
    
    def __autoCompletionWordSeparators(self, language):
        """
        Private method to get the word separator characters for a language.
        
        @param language language of the APIs object (string)
        @return word separator characters (list of strings)
        """
        return self.__wseps.get(language, None)
    
    def abort(self):
        """
        Public method to ask the thread to stop.
        """
        self.__aborted = True
    
    def __loadApiFileIfNewer(self, apiFile):
        """
        Private method to load an API file, if it is newer than the one read
        into the database.
        
        @param apiFile filename of the raw API file (string)
        """
        db = QSqlDatabase.database(self.__language)
        db.transaction()
        try:
            query = QSqlQuery(db)
            query.prepare(self.file_loaded_stmt)
            query.bindValue(":file", apiFile)
            query.exec_()
            if query.next() and query.isValid():
                loadTime = QDateTime.fromString(query.value(0), Qt.ISODate)
            else:
                loadTime = QDateTime(1970, 1, 1, 0, 0)
            del query
        finally:
            db.commit()
        if self.__projectPath:
            modTime = QFileInfo(os.path.join(self.__projectPath, apiFile)).lastModified()
        else:
            modTime = QFileInfo(apiFile).lastModified()
            basesFile = os.path.splitext(apiFile)[0] + ".bas"
            if os.path.exists(basesFile):
                modTimeBases = QFileInfo(basesFile).lastModified()
                if modTimeBases > modTime:
                    modTime = modTimeBases
        if loadTime < modTime:
            self.__loadApiFile(apiFile)
    
    def __loadApiFile(self, apiFile):
        """
        Private method to read a raw API file into the database.
        
        @param apiFile filename of the raw API file (string)
        """
        apis = []
        bases = []
        
        if self.__language == ApisNameProject:
            try:
                module = Utilities.ModuleParser.readModule(
                    os.path.join(self.__projectPath, apiFile),
                    basename=self.__projectPath + os.sep,
                    caching=False)
                language = module.getType()
                if language:
                    apiGenerator = APIGenerator(module)
                    apis = apiGenerator.genAPI(True, "", True)
                    try:
                        basesDict = apiGenerator.genBases(True)
                        for baseEntry in basesDict:
                            if basesDict[baseEntry]:
                                bases.append("{0} {1}\n".format(
                                    baseEntry, " ".join(sorted(basesDict[baseEntry]))))
                    except AttributeError:
                        # eric 5.1 doesn't have this method
                        pass
            except (IOError, ImportError):
                pass
        else:
            try:
                apis = Utilities.readEncodedFile(apiFile)[0].splitlines(True)
            except (IOError, UnicodeError):
                pass
            try:
                basesFile = os.path.splitext(apiFile)[0] + ".bas"
                if os.path.exists(basesFile):
                    bases = Utilities.readEncodedFile(basesFile)[0].splitlines(True)
            except (IOError, UnicodeError):
                pass
            language = None
        
        if len(apis) > 0:
            self.__storeApis(apis, bases, apiFile, language)
    
    def __storeApis(self, apis, bases, apiFile, language):
        """
        Private method to put the API entries into the database.
        
        @param apis list of api entries (list of strings)
        @param bases list of base class entries (list of strings)
        @param apiFile filename of the file read to get the APIs (string)
        @param language programming language of the file of the APIs (string)
        """
        if language:
            wseps = self.__autoCompletionWordSeparators(language)
        else:
            wseps = self.__proxy.autoCompletionWordSeparators()
        if wseps is None:
            return

        db = QSqlDatabase.database(self.__language)
        db.transaction()
        try:
            query = QSqlQuery(db)
            # step 1: create entry in file table and get the ID
            query.prepare(self.populate_file_stmt)
            query.bindValue(":file", apiFile)
            query.exec_()
            query.prepare(self.file_id_stmt)
            query.bindValue(":file", apiFile)
            query.exec_()
            query.next()
            id = int(query.value(0))
            
            # step 2: delete all entries belonging to this file
            query.prepare(self.populate_del_api_stmt)
            query.bindValue(":fileId", id)
            query.exec_()
            
            query.prepare(self.populate_del_bases_stmt)
            query.bindValue(":fileId", id)
            query.exec_()
            
            # step 3: load the given API info
            query.prepare(self.populate_api_stmt)
            for api in apis:
                if self.__aborted:
                    break
                
                api = api.strip()
                if len(api) == 0:
                    continue
                
                b = api.find('(')
                if b == -1:
                    path = api
                    sig = ""
                else:
                    path = api[:b]
                    sig = api[b:]
                
                while len(path) > 0:
                    acWord = ""
                    context = ""
                    fullContext = ""
                    pictureId = ""
                    
                    # search for word separators
                    index = len(path)
                    while index > 0:
                        index -= 1
                        found = False
                        for wsep in wseps:
                            if path[:index].endswith(wsep):
                                found = True
                                break
                        if found:
                            if acWord == "":
                                # completion found
                                acWord = path[index:]
                                path = path[:(index - len(wsep))]
                                index = len(path)
                                fullContext = path
                                context = path
                                try:
                                    acWord, pictureId = acWord.split("?", 1)
                                except ValueError:
                                    pass
                            else:
                                context = path[index:]
                                break
                    # none found?
                    if acWord == "":
                        acWord = path
                        path = ""
                    
                    query.bindValue(":acWord", acWord)
                    query.bindValue(":context", context)
                    query.bindValue(":fullContext", fullContext)
                    query.bindValue(":signature", sig)
                    query.bindValue(":fileId", id)
                    query.bindValue(":pictureId", pictureId)
                    query.exec_()
                    
                    sig = ""
            
            # step 4: load the given base classes info
            query.prepare(self.populate_bases_stmt)
            for base in bases:
                if self.__aborted:
                    break
                
                base = base.strip()
                if len(base) == 0:
                    continue
                
                class_, baseClasses = base.split(" ", 1)
                query.bindValue(":class", class_)
                query.bindValue(":baseClasses", baseClasses)
                query.bindValue(":fileId", id)
                query.exec_()
            
            if not self.__aborted:
                # step 4: update the file entry
                query.prepare(self.update_file_stmt)
                query.bindValue(":lastRead", QDateTime.currentDateTime())
                query.bindValue(":file", apiFile)
                query.exec_()
        finally:
            del query
            if self.__aborted:
                db.rollback()
            else:
                db.commit()
    
    def __deleteApiFile(self, apiFile):
        """
        Private method to delete all references to an api file.
        
        @param apiFile filename of the raw API file (string)
        """
        db = QSqlDatabase.database(self.__language)
        db.transaction()
        try:
            query = QSqlQuery(db)
            
            # step 1: get the ID belonging to the api file
            query.prepare(self.file_id_stmt)
            query.bindValue(":file", apiFile)
            query.exec_()
            query.next()
            id = int(query.value(0))
            
            # step 2: delete all API entries belonging to this file
            query.prepare(self.populate_del_api_stmt)
            query.bindValue(":fileId", id)
            query.exec_()
            
            # step 3: delete all base classes entries belonging to this file
            query.prepare(self.populate_del_bases_stmt)
            query.bindValue(":fileId", id)
            query.exec_()
            
            # step 4: delete the file entry
            query.prepare(self.file_delete_id_stmt)
            query.bindValue(":id", id)
            query.exec_()
        finally:
            del query
            db.commit()
    
    def run(self):
        """
        Public method to perform the threads work.
        """
        QCoreApplication.postEvent(self.__proxy, QEvent(QEvent.Type(WorkerStarted)))
        
        db = QSqlDatabase.database(self.__language)
        if db.isValid() and db.isOpen():
            # step 1: remove API files not wanted any longer
            if not self.__refresh:
                loadedApiFiles = self.__proxy.getApiFiles()
                for apiFile in loadedApiFiles:
                    if not self.__aborted and apiFile not in self.__apiFiles:
                        self.__deleteApiFile(apiFile)
            
            # step 2: (re-)load api files
            for apiFile in self.__apiFiles:
                if not self.__aborted:
                    self.__loadApiFileIfNewer(apiFile)
        
        if self.__aborted:
            QCoreApplication.postEvent(self.__proxy, QEvent(QEvent.Type(WorkerAborted)))
        else:
            QCoreApplication.postEvent(self.__proxy, QEvent(QEvent.Type(WorkerFinished)))


class DbAPIs(QObject):
    """
    Class implementing an API storage entity.
    
    @signal apiPreparationFinished() emitted after the API preparation has finished
    @signal apiPreparationStarted() emitted after the API preparation has started
    @signal apiPreparationCancelled() emitted after the API preparation has been
            cancelled
    """
    apiPreparationFinished = pyqtSignal()
    apiPreparationStarted = pyqtSignal()
    apiPreparationCancelled = pyqtSignal()
    
    DB_VERSION = 4
    
    create_mgmt_stmt = """
        CREATE TABLE mgmt
        (format INTEGER)
    """
    drop_mgmt_stmt = """DROP TABLE IF EXISTS mgmt"""
    
    create_api_stmt = """
        CREATE TABLE api
        (acWord TEXT,
         context TEXT,
         fullContext TEXT,
         signature TEXT,
         fileId INTEGER,
         pictureId INTEGER,
         UNIQUE(acWord, fullContext, signature) ON CONFLICT IGNORE
        )
    """
    drop_api_stmt = """DROP TABLE IF EXISTS api"""
    
    create_bases_stmt = """
        CREATE TABLE bases
        (class TEXT UNIQUE ON CONFLICT IGNORE,
         baseClasses TEXT,
         fileId INTEGER
        )
    """
    drop_bases_stmt = """DROP TABLE IF EXISTS bases"""
    
    create_file_stmt = """
        CREATE TABLE file
        (id INTEGER PRIMARY KEY AUTOINCREMENT,
         file TEXT UNIQUE ON CONFLICT IGNORE,
         lastRead TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """
    drop_file_stmt = """DROP TABLE IF EXISTS file"""
    
    create_acWord_idx = """CREATE INDEX acWord_idx on api (acWord)"""
    drop_acWord_idx = """DROP INDEX IF EXISTS acWord_idx"""
    
    create_context_idx = """CREATE INDEX context_idx on api (context)"""
    drop_context_idx = """DROP INDEX IF EXISTS context_idx"""
    
    create_fullContext_idx = """CREATE INDEX fullContext_idx on api (fullContext)"""
    drop_fullContext_idx = """DROP INDEX IF EXISTS fullContext_idx"""
    
    create_bases_idx = """CREATE INDEX base_idx on bases (class)"""
    drop_bases_idx = """DROP INDEX IF EXISTS base_idx"""
    
    create_file_idx = """CREATE INDEX file_idx on file (file)"""
    drop_file_idx = """DROP INDEX IF EXISTS file_idx"""

    api_files_stmt = """
        SELECT file FROM file
    """

    ac_stmt = """
        SELECT DISTINCT acWord, fullContext, pictureId FROM api
        WHERE acWord GLOB :acWord
        ORDER BY acWord
    """
    ac_context_stmt = """
        SELECT DISTINCT acWord, fullContext, pictureId FROM api
        WHERE context = :context
        ORDER BY acWord
    """
    ac_context_word_stmt = """
        SELECT DISTINCT acWord, fullContext, pictureId FROM api
        WHERE acWord GLOB :acWord AND context = :context
        ORDER BY acWord
    """
    bases_stmt = """
        SELECT baseClasses from bases
        WHERE class = :class
    """
    ct_stmt = """
        SELECT DISTINCT acWord, signature, fullContext FROM api
        WHERE acWord = :acWord
    """
    ct_context_stmt = """
        SELECT DISTINCT acWord, signature, fullContext FROM api
        WHERE acWord = :acWord
        AND context = :context
    """
    ct_fullContext_stmt = """
        SELECT DISTINCT acWord, signature, fullContext FROM api
        WHERE acWord = :acWord
        AND fullContext = :fullContext
    """
    format_select_stmt = """
        SELECT format FROM mgmt
    """
    mgmt_insert_stmt = """
        INSERT INTO mgmt (format) VALUES ({0:d})
    """.format(DB_VERSION)
    
    def __init__(self, language, parent=None):
        """
        Constructor
        
        @param language language of the APIs object (string)
        @param parent reference to the parent object (QObject)
        """
        QObject.__init__(self, parent)
        self.setObjectName("DbAPIs_{0}".format(language))
        
        self.__inPreparation = False
        self.__worker = None
        self.__workerQueue = []
        
        self.__language = language
        if self.__language == ApisNameProject:
            self.__initAsProject()
        else:
            self.__initAsLanguage()
    
    def __initAsProject(self):
        """
        Private method to initialize as a project API object.
        """
        self.__lexer = None
        
        self.__project = e5App().getObject("Project")
        self.__project.projectOpened.connect(self.__projectOpened)
        self.__project.newProject.connect(self.__projectOpened)
        self.__project.projectClosed.connect(self.__projectClosed)
        if self.__project.isOpen():
            self.__projectOpened()
    
    def __initAsLanguage(self):
        """
        Private method to initialize as a language API object.
        """
        if self.__language in ["Python", "Python2", "Python3"]:
            self.__discardFirst = "self"
            # TODO: discard first can be 'cls' as well
        else:
            self.__discardFirst = ""
        self.__lexer = QScintilla.Lexers.getLexer(self.__language)
        self.__apifiles = Preferences.getEditorAPI(self.__language)
        self.__apifiles.sort()
        if self.__lexer is not None:
            self.__openAPIs()
    
    def _apiDbName(self):
        """
        Protected method to determine the name of the database file.
        
        @return name of the database file (string)
        """
        if self.__language == ApisNameProject:
            return os.path.join(self.__project.getProjectManagementDir(),
                                "project-apis.db")
        else:
            apiDir = os.path.join(Utilities.getConfigDir(), "APIs")
            if not os.path.exists(apiDir):
                os.makedirs(apiDir)
            return os.path.join(apiDir, "{0}-api.db".format(self.__language))
    
    def close(self):
        """
        Public method to close the database.
        """
        self.__workerQueue = []
        if self.__worker is not None:
            self.__worker.abort()
            if self.__worker is not None:
                self.__worker.wait(5000)
                if self.__worker is not None and \
                   not self.__worker.isFinished():
                    self.__worker.terminate()
                    if self.__worker is not None:
                        self.__worker.wait(5000)
        
        QSqlDatabase.database(self.__language).close()
        QSqlDatabase.removeDatabase(self.__language)
        
    def __openApiDb(self):
        """
        Private method to open the API database.
        """
        db = QSqlDatabase.database(self.__language, False)
        if not db.isValid():
            # the database connection is a new one
            db = QSqlDatabase.addDatabase("QSQLITE", self.__language)
            db.setDatabaseName(self._apiDbName())
            db.open()
    
    def __createApiDB(self):
        """
        Private method to create an API database.
        """
        db = QSqlDatabase.database(self.__language)
        db.transaction()
        try:
            query = QSqlQuery(db)
            # step 1: drop old tables
            query.exec_(self.drop_mgmt_stmt)
            query.exec_(self.drop_api_stmt)
            query.exec_(self.drop_bases_stmt)
            query.exec_(self.drop_file_stmt)
            # step 2: drop old indices
            query.exec_(self.drop_acWord_idx)
            query.exec_(self.drop_context_idx)
            query.exec_(self.drop_fullContext_idx)
            query.exec_(self.drop_bases_idx)
            query.exec_(self.drop_file_idx)
            # step 3: create tables
            query.exec_(self.create_api_stmt)
            query.exec_(self.create_bases_stmt)
            query.exec_(self.create_file_stmt)
            query.exec_(self.create_mgmt_stmt)
            query.exec_(self.mgmt_insert_stmt)
            # step 4: create indices
            query.exec_(self.create_acWord_idx)
            query.exec_(self.create_context_idx)
            query.exec_(self.create_fullContext_idx)
            query.exec_(self.create_bases_idx)
            query.exec_(self.create_file_idx)
        finally:
            del query
            db.commit()

    def getApiFiles(self):
        """
        Public method to get a list of API files loaded into the database.
        
        @return list of API filenames (list of strings)
        """
        apiFiles = []
        
        db = QSqlDatabase.database(self.__language)
        db.transaction()
        try:
            query = QSqlQuery(db)
            query.exec_(self.api_files_stmt)
            while query.next():
                apiFiles.append(query.value(0))
        finally:
            del query
            db.commit()
        
        return apiFiles
    
    def __isPrepared(self):
        """
        Private method to check, if the database has been prepared.
        
        @return flag indicating the prepared status (boolean)
        """
        db = QSqlDatabase.database(self.__language)
        prepared = len(db.tables()) > 0
        if prepared:
            db.transaction()
            prepared = False
            try:
                query = QSqlQuery(db)
                ok = query.exec_(self.format_select_stmt)
                if ok:
                    query.next()
                    format = int(query.value(0))
                    if format >= self.DB_VERSION:
                        prepared = True
            finally:
                del query
                db.commit()
        return prepared
    
    def getCompletions(self, start=None, context=None, followHierarchy=False):
        """
        Public method to determine the possible completions.
        
        @keyparam start string giving the start of the word to be
            completed (string)
        @keyparam context string giving the context (e.g. classname)
            to be completed (string)
        @keyparam followHierarchy flag indicating to follow the hierarchy of
            base classes (boolean)
        @return list of dictionaries with possible completions (key 'completion'
            contains the completion (string), key 'context'
            contains the context (string) and key 'pictureId'
            contains the ID of the icon to be shown (string))
        """
        completions = []
        
        db = QSqlDatabase.database(self.__language)
        if db.isOpen() and not self.__inPreparation:
            db.transaction()
            try:
                query = None
                
                if start is not None and context is not None:
                    query = QSqlQuery(db)
                    query.prepare(self.ac_context_word_stmt)
                    query.bindValue(":acWord", start + '*')
                    query.bindValue(":context", context)
                elif start is not None:
                    query = QSqlQuery(db)
                    query.prepare(self.ac_stmt)
                    query.bindValue(":acWord", start + '*')
                elif context is not None:
                    query = QSqlQuery(db)
                    query.prepare(self.ac_context_stmt)
                    query.bindValue(":context", context)
                
                if query is not None:
                    query.exec_()
                    while query.next():
                        completions.append({"completion": query.value(0),
                                            "context": query.value(1),
                                            "pictureId": query.value(2)})
                    del query
            finally:
                db.commit()
            
            if followHierarchy:
                query = QSqlQuery(db)
                query.prepare(self.bases_stmt)
                query.bindValue(":class", context)
                query.exec_()
                if query.next():
                    bases = query.value(0).split()
                else:
                    bases = []
                for base in bases:
                    completions.extend(self.getCompletions(start, base,
                                       followHierarchy=True))
        
        return completions
    
    def getCalltips(self, acWord, commas, context=None, fullContext=None,
                    showContext=True, followHierarchy=False):
        """
        Public method to determine the calltips.
        
        @param acWord function to get calltips for (string)
        @param commas minimum number of commas contained in the calltip (integer)
        @param context string giving the context (e.g. classname) (string)
        @param fullContext string giving the full context (string)
        @param showContext flag indicating to show the calltip context (boolean)
        @keyparam followHierarchy flag indicating to follow the hierarchy of
            base classes (boolean)
        @return list of calltips (list of string)
        """
        calltips = []
        
        db = QSqlDatabase.database(self.__language)
        if db.isOpen() and not self.__inPreparation:
            if self.autoCompletionWordSeparators():
                contextSeparator = self.autoCompletionWordSeparators()[0]
            else:
                contextSeparator = " "
            db.transaction()
            try:
                query = QSqlQuery(db)
                if fullContext:
                    query.prepare(self.ct_fullContext_stmt)
                    query.bindValue(":fullContext", fullContext)
                elif context:
                    query.prepare(self.ct_context_stmt)
                    query.bindValue(":context", context)
                else:
                    query.prepare(self.ct_stmt)
                query.bindValue(":acWord", acWord)
                query.exec_()
                while query.next():
                    word = query.value(0)
                    sig = query.value(1)
                    fullCtx = query.value(2)
                    if sig:
                        if self.__discardFirst:
                            sig = "({0}".format(sig[1:])\
                                    .replace(self.__discardFirst, "", 1)\
                                    .strip(", \t\r\n")
                        if self.__enoughCommas(sig, commas):
                            if showContext:
                                calltips.append("{0}{1}{2}{3}".format(
                                    fullCtx, contextSeparator, word, sig))
                            else:
                                calltips.append("{0}{1}".format(word, sig))
                del query
            finally:
                db.commit()
            
            if followHierarchy:
                query = QSqlQuery(db)
                query.prepare(self.bases_stmt)
                query.bindValue(":class", context)
                query.exec_()
                if query.next():
                    bases = query.value(0).split()
                else:
                    bases = []
                for base in bases:
                    calltips.extend(self.getCalltips(acWord, commas, context=base,
                                                     showContext=showContext,
                                                     followHierarchy=True))
            
            if context and len(calltips) == 0 and not followHierarchy:
                # nothing found, try without a context
                calltips = self.getCalltips(acWord, commas, showContext=showContext)
        
        return calltips
    
    def __enoughCommas(self, s, commas):
        """
        Private method to determine, if the given string contains enough commas.
        
        @param s string to check (string)
        @param commas number of commas to check for (integer)
        @return flag indicating, that there are enough commas (boolean)
        """
        end = s.find(')')
        
        if end < 0:
            return False
        
        w = s[:end]
        return w.count(',') >= commas
    
    def __openAPIs(self):
        """
        Private method to open the API database.
        """
        self.__openApiDb()
        if not self.__isPrepared():
            self.__createApiDB()
        
        # prepare the database if neccessary
        self.prepareAPIs()
    
    def prepareAPIs(self, rawList=None):
        """
        Public method to prepare the APIs if neccessary.
        
        @keyparam rawList list of raw API files (list of strings)
        """
        if self.__inPreparation:
            return
        
        projectPath = ""
        if rawList:
            apiFiles = rawList[:]
        elif self.__language == ApisNameProject:
            apiFiles = self.__project.getSources()
            projectPath = self.__project.getProjectPath()
        else:
            apiFiles = Preferences.getEditorAPI(self.__language)
        self.__worker = DbAPIsWorker(self, self.__language, apiFiles, projectPath)
        self.__worker.start()
    
    def __processQueue(self):
        """
        Private slot to process the queue of files to load.
        """
        if self.__worker is not None and self.__worker.isFinished():
            self.__worker = None
        
        if self.__worker is None and len(self.__workerQueue) > 0:
            apiFiles = [self.__workerQueue.pop(0)]
            if self.__language == ApisNameProject:
                projectPath = self.__project.getProjectPath()
                apiFiles = [apiFiles[0].replace(projectPath + os.sep, "")]
            else:
                projectPath = ""
            self.__worker = DbAPIsWorker(self, self.__language, apiFiles, projectPath,
                                         refresh=True)
            self.__worker.start()
    
    def getLexer(self):
        """
        Public method to return a reference to our lexer object.
        
        @return reference to the lexer object (QScintilla.Lexers.Lexer)
        """
        return self.__lexer
    
    def autoCompletionWordSeparators(self):
        """
        Private method to get the word separator characters.
        
        @return word separator characters (list of strings)
        """
        if self.__lexer:
            return self.__lexer.autoCompletionWordSeparators()
        return None
    
    def event(self, evt):
        """
        Protected method to handle events from the worker thread.
        
        @param evt reference to the event object (QEvent)
        @return flag indicating, if the event was handled (boolean)
        """
        if evt.type() == WorkerStarted:
            self.__inPreparation = True
            self.apiPreparationStarted.emit()
            return True
        
        elif evt.type() == WorkerFinished:
            self.__inPreparation = False
            self.apiPreparationFinished.emit()
            QTimer.singleShot(0, self.__processQueue)
            return True
        
        elif evt.type() == WorkerAborted:
            self.__inPreparation = False
            self.apiPreparationCancelled.emit()
            QTimer.singleShot(0, self.__processQueue)
            return True
        
        else:
            return QObject.event(self,  evt)
    
    ########################################################
    ## project related stuff below
    ########################################################
    
    def __projectOpened(self):
        """
        Private slot to perform actions after a project has been opened.
        """
        if self.__project.getProjectLanguage() in ["Python", "Python3"]:
            self.__discardFirst = "self"
        else:
            self.__discardFirst = ""
        self.__lexer = QScintilla.Lexers.getLexer(self.__project.getProjectLanguage())
        self.__openAPIs()
    
    def __projectClosed(self):
        """
        Private slot to perform actions after a project has been closed.
        """
        self.close()
    
    def editorSaved(self, filename):
        """
        Public slot to handle the editorSaved signal.
        
        @param filename name of the file that was saved (string)
        """
        if self.__project.isProjectSource(filename):
            self.__workerQueue.append(filename)
            self.__processQueue()


class APIsManager(QObject):
    """
    Class implementing the APIsManager class, which is the central store for
    API information used by autocompletion and calltips.
    """
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent object (QObject)
        """
        QObject.__init__(self, parent)
        self.setObjectName("APIsManager")
        
        # initialize the apis dictionary
        self.__apis = {}
    
    def reloadAPIs(self):
        """
        Public slot to reload the api information.
        """
        for api in list(self.__apis.values()):
            api and api.prepareAPIs()
    
    def getAPIs(self, language):
        """
        Public method to get an apis object for autocompletion/calltips.
        
        This method creates and loads an APIs object dynamically upon request.
        This saves memory for languages, that might not be needed at the moment.
        
        @param language the language of the requested api object (string)
        @return the apis object (APIs)
        """
        try:
            return self.__apis[language]
        except KeyError:
            if language in QScintilla.Lexers.getSupportedLanguages() or \
               language == ApisNameProject:
                # create the api object
                self.__apis[language] = DbAPIs(language)
                return self.__apis[language]
            else:
                return None
    
    def deactivate(self):
        """
        Public method to perform actions upon deactivation.
        """
        for apiLang in self.__apis:
            self.__apis[apiLang].close()
            self.__apis[apiLang] = None

eric ide

mercurial