Beispiel #1
0
class TextEditHandler(logging.Handler):
    colorMap = {
        logging.DEBUG: Qt.gray,
        logging.INFO: Qt.green,
        logging.WARNING: Qt.darkYellow,
        logging.ERROR: Qt.red,
    }

    PREFIX_MAX_SIZE = 30
    VALID_PREFIX = re.compile(r'[^\w ]')

    @contextmanager
    def useForLogger(self, loggerName: str):
        self.reset()
        tagLogger = logging.getLogger(loggerName)
        tagLogger.addHandler(self)
        yield
        tagLogger.removeHandler(self)

    def __init__(self,
                 textEdit: QTextEdit,
                 treeView: QTreeView,
                 filterLineEdit: QLineEdit = None):
        super().__init__()
        self.treeView = treeView
        self.textEdit = textEdit
        self.filterLineEdit = filterLineEdit

        self.model = QStandardItemModel(self.treeView)
        self.filterModel = QSortFilterProxyModel(self.treeView)
        self.filterModel.setSourceModel(self.model)
        self.filterModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.filterModel.setRecursiveFilteringEnabled(True)
        self.treeView.setModel(self.filterModel)

        if self.filterLineEdit is not None:
            self.filterLineEdit.textChanged.connect(
                self.filterModel.setFilterRegExp)
        self.treeView.setHeaderHidden(True)

        self.topItem = None
        self._groups: List[str] = []
        self._prefix: Dict[str, QStandardItem] = {}
        self.reset()

    def reset(self):
        self.model.clear()
        self.topItem = QStandardItem('Top Item')
        self.model.setItem(0, 0, self.topItem)
        self._groups.clear()
        self._prefix.clear()

    def emit(self, record: logging.LogRecord):
        msg = self.format(record)
        color = self.colorMap.get(record.levelno, Qt.black)
        self.addText(msg, color)
        self.addGroup(msg)
        self.treeView.expandToDepth(0)

    def addText(self, text: str, color: QColor = None):
        with changeColor(self.textEdit, color):
            self.textEdit.insertPlainText(text + '\n')
            self.textEdit.moveCursor(QTextCursor.End)

    def addGroup(self, text: str):
        self._groups.append(text)
        validPrefix = self.VALID_PREFIX.split(text, maxsplit=1)[0]

        item = QStandardItem(text)
        if existingItem := self._findItemWithExistingPrefix(validPrefix):
            parentItem = existingItem

        elif biggestPrefix := self._getBiggestCommonPrefix(validPrefix):
            assert biggestPrefix is not None
            item, biggestPrefixText, prefixSize = biggestPrefix

            parentItem = self._addPrefix(validPrefix, prefixSize)
            self._moveItems(parentItem, validPrefix)
Beispiel #2
0
class MainModel():
    '''Data model for pyspector.'''

    def __init__(self):
        '''Initializes a MainModel instance.'''
        self._searchText = ''
        self._matchCase = False
        self._includePrivateMembers = False
        self._includeInheritedMembers = False
        self._sortByType = True

        # Initialize icons.
        iconDir = f'{dirname(dirname(__file__))}/icons'
        self._icons = {
            'module': QIcon(f'{iconDir}/module.svg'),
            'abstract base class': QIcon(f'{iconDir}/abstract.svg'),
            'class': QIcon(f'{iconDir}/class.svg'),
            'function': QIcon(f'{iconDir}/function.svg'),
            'property': QIcon(f'{iconDir}/property.svg'),
            'object': QIcon(f'{iconDir}/object.svg')
        }

        # Create the unfiltered tree model.
        self._treeModel = QStandardItemModel()

        # Create regular expressions that exclude or include private members.
        self._excludePrivateRegEx = QRegularExpression('^[^_]|^__')
        self._includePrivateRegEx = QRegularExpression('')

        # Create regular expressions that exclude or include inherited members.
        self._excludeInheritedRegEx = QRegularExpression('^$')
        self._includeInheritedRegEx = QRegularExpression('')

        # Create a filtered tree model used to exclude or include private members.
        self._intermediateTreeModel = QSortFilterProxyModel()
        self._intermediateTreeModel.setSourceModel(self._treeModel)
        privateRegEx = self._includePrivateRegEx if self._includePrivateMembers else self._excludePrivateRegEx
        self._intermediateTreeModel.setFilterRegularExpression(privateRegEx)

        # Create a filtered tree model used to exclude or include inherited members.
        self._secondIntermediateTreeModel = QSortFilterProxyModel()
        self._secondIntermediateTreeModel.setSourceModel(self._intermediateTreeModel)
        self._secondIntermediateTreeModel.setFilterKeyColumn(2)
        inheritedRegEx = self._includeInheritedRegEx if self._includeInheritedMembers else self._excludeInheritedRegEx
        self._secondIntermediateTreeModel.setFilterRegularExpression(inheritedRegEx)

        # Create a filtered tree model that matches the search text.
        self._filteredTreeModel = QSortFilterProxyModel()
        self._filteredTreeModel.setSourceModel(self._secondIntermediateTreeModel)
        self._filteredTreeModel.setRecursiveFilteringEnabled(True)
        self._filteredTreeModel.setFilterFixedString(self._searchText)
        self._filteredTreeModel.setFilterCaseSensitivity(1 if self._matchCase else 0)

    @property
    def filteredTreeModel(self) -> QSortFilterProxyModel:
        '''The filtered version of the tree model.'''
        return self._filteredTreeModel

    @property
    def searchText(self) -> str:
        '''The current search text.'''
        return self._searchText

    @searchText.setter
    def searchText(self, value: str) -> None:
        self._searchText = value
        self._filteredTreeModel.setFilterFixedString(value)

    @property
    def matchCase(self) -> bool:
        '''Whether or not case-sensitive matching is used.'''
        return self._matchCase

    @matchCase.setter
    def matchCase(self, value: bool) -> None:
        self._matchCase = value
        self._filteredTreeModel.setFilterCaseSensitivity(1 if value else 0)

    @property
    def includePrivateMembers(self) -> bool:
        '''Whether or not private members (beginning with "_") are included in the tree.'''
        return self._includePrivateMembers

    @includePrivateMembers.setter
    def includePrivateMembers(self, value: bool) -> None:
        self._includePrivateMembers = value
        regEx = self._includePrivateRegEx if value else self._excludePrivateRegEx
        self._intermediateTreeModel.setFilterRegularExpression(regEx)

    @property
    def includeInheritedMembers(self) -> bool:
        '''Whether or not inherited members are included in the tree.'''
        return self._includeInheritedMembers

    @includeInheritedMembers.setter
    def includeInheritedMembers(self, value: bool) -> None:
        self._includeInheritedMembers = value
        regEx = self._includeInheritedRegEx if value else self._excludeInheritedRegEx
        self._secondIntermediateTreeModel.setFilterRegularExpression(regEx)

    @property
    def sortByType(self) -> bool:
        '''Whether or not  members are sorted by type.'''
        return self._sortByType

    @sortByType.setter
    def sortByType(self, value: bool) -> None:
        self._sortByType = value
        self._sort()

    def setModuleNames(self, moduleNames) -> None:
        '''Adds the specified modules to the tree, removing any that are no longer needed.'''
        # Remove any modules that aren't in the list.
        rootItem: QStandardItem = self._treeModel.invisibleRootItem()
        for i in range(rootItem.rowCount() - 1, -1, -1):
            if rootItem.child(i).text() not in moduleNames:
                rootItem.removeRow(i)

        # Add all the modules in the list.
        for moduleName in moduleNames:
            self._addModule(moduleName)

        # Sort.
        self._sort()

    def _sort(self) -> None:
        # Sort all items alphabetically by name.
        self._treeModel.sort(0)

        # Optionally, sort by type.
        if self.sortByType:
            self._treeModel.sort(1)

    def _dumpTree(self, parentIndex = QModelIndex(), depth = 0) -> None:
        rowCount = self._treeModel.rowCount(parentIndex)
        for row in range(rowCount):
            index = self._treeModel.index(row, 0, parentIndex)
            item = self._treeModel.itemFromIndex(index)
            indent = '  ' * depth
            id = item.data()['id']
            print(f'{indent}{id}')
            self._dumpTree(index, depth + 1)

    def findItemByName(self, name: str) -> QModelIndex:
        '''Finds the item with the specified name.'''

        # Create search predicates for matching and containing name.
        if self.matchCase:
            itemHasName = lambda item: item.text() == name
            itemContainsName = lambda  item: name in item.text()
        else:
            nameNoCase = name.casefold()
            itemHasName = lambda item: item.text().casefold() == nameNoCase
            itemContainsName = lambda item: nameNoCase in item.text().casefold()

        # Try for a full match, then a partial match.
        for predicate in [itemHasName, itemContainsName]:
            index = utilities.findIndexInModel(self._filteredTreeModel, predicate)
            if index.isValid():
                break

        return index

    def findItemById(self, id: str) -> QModelIndex:
        '''Finds the item with the specified ID.'''
        print(f'looking for item with id {id}')
        predicate = lambda item: item.data()['id'] == id
        index = utilities.findIndexInModel(self._filteredTreeModel, predicate)
        return index

    def _addModule(self, moduleName, depth = 0):
        # Check to see if module has already been added.
        rootItem = self._treeModel.invisibleRootItem()
        if self._parentContainsItem(rootItem, moduleName):
            return

        try:
            module = importlib.import_module(moduleName)
            item = self._addItem(rootItem, moduleName, moduleName, 'module', module)
            self._inspectObject(item, module, depth)
        except:
            self._addItem(rootItem, moduleName, moduleName, 'module', None, error = 'Could not import module.')

    def _parentContainsItem(self, parentItem: QStandardItem, id: str) -> bool:
        for row in range(parentItem.rowCount()):
            childId = parentItem.child(row).data()['id']
            if childId == id:
                return True
        return False

    def _inspectObject(self, parentItem: QStandardItem, obj: object, depth: int) -> None:
        '''Recursively adds object to the hierarchical model.'''
        for (memberName, memberValue) in inspect.getmembers(obj):
            memberType = self._getMemberType(memberValue)

            # Skip "magic" members that are classes -- they cause problems.
            if memberName.startswith('__') and memberType == 'class':
                continue

            # Skip modules within modules.
            if memberType == 'module':
                # TODO: Should we add nested modules? Seems useful, but leads to a segfault in the
                # case of matplotlib. 
                #print(f'{"  "*depth}adding nested module {memberName}: {memberValue.__name__}')
                #self._addModule(memberValue, depth + 1)
                continue

            # Don't add the same item twice.
            parentId = parentItem.data()['id']
            id = f'{parentId}/{memberName}'
            if self._parentContainsItem(parentItem, id):
                continue

            # Check inheritance of class members.
            inheritance = 'inherited' if inspect.isclass(obj) and memberName not in obj.__dict__ else ''

            # For functions, try to include the signature in the name.
            name = memberName
            if memberType == 'function':
                try:
                    name += str(inspect.signature(memberValue))
                except:
                    pass

            # Add an item for the current member.
            item = self._addItem(parentItem, id, name, memberType, memberValue, inheritance)

            # Recurse into classes (but not if it's the same class we're inspecting).
            if 'class' in memberType and memberValue != obj:
                print(f'{"  "*depth}inspecting class {memberName} in module {memberValue.__module__}')
                self._inspectObject(item, memberValue, depth + 1)

            # Recurse into property getter, setter, deleter functions.
            # TODO: Generalize this to data descriptors other than just the 'property' class.
            if type(memberValue) == property:
                if memberValue.fget:
                    self._addItem(item, f'{id}/get', '[get]', 'function', memberValue.fget)
                if memberValue.fset:
                    self._addItem(item, f'{id}/set', '[set]', 'function', memberValue.fset)
                if memberValue.fdel:
                    self._addItem(item, f'{id}/delete', '[delete]', 'function', memberValue.fdel)

    def _addItem(self, parentItem: QStandardItem, id: str, name: str, type: str, value: object, inheritance: str = '', error: str = '') -> QStandardItem:
        '''Adds one model item to a parent model item.'''
        key = type if type in self._icons else 'object'
        item1 = QStandardItem(self._icons[key], name)
        item1.setData({ 'id': id, 'type': type, 'value': value, 'error': error })
        item1.setEditable(False)
        if len(error):
            item1.setBackground(QBrush(QColor(255, 0, 0, 64)))
        item2 = QStandardItem(type)
        item2.setEditable(False)
        item3 = QStandardItem(inheritance)
        item3.setEditable(False)
        parentItem.appendRow([item1, item2, item3])
        return item1

    def _getMemberType(self, memberValue: object) -> str:
        '''Attempts to determine the type of a member from its value.'''
        if inspect.ismodule(memberValue):
            return 'module'
        if inspect.isabstract(memberValue):
            return 'abstract base class'
        if inspect.isclass(memberValue):
            return 'class'
        if inspect.isfunction(memberValue) or inspect.isbuiltin(memberValue) or inspect.isroutine(memberValue):
            return 'function'
        if inspect.isdatadescriptor(memberValue):
            return 'property'
        return 'object'