"""A utility class for a code container. A code container is a class which holds source code for a debugger. It knows how to color the text, and also how to translate lines into offsets, and back. """ from __future__ import annotations import os import sys import tokenize from keyword import kwlist from typing import Any import win32api import winerror from win32com.axdebug import axdebug, contexts from win32com.axdebug.util import _wrap from win32com.server.exception import COMException _keywords = { _keyword for _keyword in kwlist # Avoids including True/False/None if _keyword.islower() } """set of Python keywords""" class SourceCodeContainer: def __init__( self, text: str | None, fileName="", sourceContext=0, startLineNumber=0, site=None, debugDocument=None, ): self.sourceContext = sourceContext # The source context added by a smart host. self.text: str | None = text if text: self._buildlines() self.nextLineNo = 0 self.fileName = fileName # Any: PyIDispatch type is not statically exposed self.codeContexts: dict[int, Any] = {} self.site = site self.startLineNumber = startLineNumber self.debugDocument = debugDocument def _Close(self): self.text = self.lines = self.lineOffsets = None self.codeContexts = None self.debugDocument = None self.site = None self.sourceContext = None def GetText(self): return self.text def GetName(self, dnt): raise NotImplementedError("You must subclass this") def GetFileName(self): return self.fileName def GetPositionOfLine(self, cLineNumber): self.GetText() # Prime us. try: return self.lineOffsets[cLineNumber] except IndexError: raise COMException(scode=winerror.S_FALSE) def GetLineOfPosition(self, charPos): self.GetText() # Prime us. lastOffset = 0 lineNo = 0 for lineOffset in self.lineOffsets[1:]: if lineOffset > charPos: break lastOffset = lineOffset lineNo += 1 else: # for not broken. # print("Can't find", charPos, "in", self.lineOffsets) raise COMException(scode=winerror.S_FALSE) # print("GLOP ret=", lineNo, (charPos - lastOffset)) return lineNo, (charPos - lastOffset) def GetNextLine(self): if self.nextLineNo >= len(self.lines): self.nextLineNo = 0 # auto-reset. return "" rc = self.lines[self.nextLineNo] self.nextLineNo += 1 return rc def GetLine(self, num): self.GetText() # Prime us. return self.lines[num] def GetNumChars(self): return len(self.GetText()) def GetNumLines(self): self.GetText() # Prime us. return len(self.lines) def _buildline(self, pos): i = self.text.find("\n", pos) if i < 0: newpos = len(self.text) else: newpos = i + 1 r = self.text[pos:newpos] return r, newpos def _buildlines(self): self.lines = [] self.lineOffsets = [0] line, pos = self._buildline(0) while line: self.lines.append(line) self.lineOffsets.append(pos) line, pos = self._buildline(pos) def _ProcessToken(self, type, token, spos, epos, line): srow, scol = spos erow, ecol = epos self.GetText() # Prime us. linenum = srow - 1 # Lines zero based for us too. realCharPos = self.lineOffsets[linenum] + scol numskipped = realCharPos - self.lastPos if numskipped == 0: pass elif numskipped == 1: self.attrs.append(axdebug.SOURCETEXT_ATTR_COMMENT) else: self.attrs.append((axdebug.SOURCETEXT_ATTR_COMMENT, numskipped)) kwSize = len(token) self.lastPos = realCharPos + kwSize attr = 0 if type == tokenize.NAME: if token in _keywords: attr = axdebug.SOURCETEXT_ATTR_KEYWORD elif type == tokenize.STRING: attr = axdebug.SOURCETEXT_ATTR_STRING elif type == tokenize.NUMBER: attr = axdebug.SOURCETEXT_ATTR_NUMBER elif type == tokenize.OP: attr = axdebug.SOURCETEXT_ATTR_OPERATOR elif type == tokenize.COMMENT: attr = axdebug.SOURCETEXT_ATTR_COMMENT # else attr remains zero... if kwSize == 0: pass elif kwSize == 1: self.attrs.append(attr) else: self.attrs.append((attr, kwSize)) def GetSyntaxColorAttributes(self): self.lastPos = 0 self.attrs = [] try: for tokens in tokenize.tokenize(self.GetNextLine): self._ProcessToken(*tokens) except tokenize.TokenError: pass # Ignore - will cause all subsequent text to be commented. numAtEnd = len(self.GetText()) - self.lastPos if numAtEnd: self.attrs.append((axdebug.SOURCETEXT_ATTR_COMMENT, numAtEnd)) return self.attrs # We also provide and manage DebugDocumentContext objects def _MakeDebugCodeContext(self, lineNo, charPos, len): return _wrap( contexts.DebugCodeContext(lineNo, charPos, len, self, self.site), axdebug.IID_IDebugCodeContext, ) # Make a context at the given position. It should take up the entire context. def _MakeContextAtPosition(self, charPos): lineNo, offset = self.GetLineOfPosition(charPos) try: endPos = self.GetPositionOfLine(lineNo + 1) except: endPos = charPos codecontext = self._MakeDebugCodeContext(lineNo, charPos, endPos - charPos) return codecontext # Returns a DebugCodeContext. debugDocument can be None for smart hosts. def GetCodeContextAtPosition(self, charPos): # trace("GetContextOfPos", charPos, maxChars) # Convert to line number. lineNo, offset = self.GetLineOfPosition(charPos) charPos = self.GetPositionOfLine(lineNo) try: cc = self.codeContexts[charPos] except KeyError: cc = self._MakeContextAtPosition(charPos) self.codeContexts[charPos] = cc return cc class SourceModuleContainer(SourceCodeContainer): def __init__(self, module): self.module = module if hasattr(module, "__file__"): fname = self.module.__file__ # Check for .pyc or .pyo or even .pys! if fname[-1] in ["O", "o", "C", "c", "S", "s"]: fname = fname[:-1] try: fname = win32api.GetFullPathName(fname) except win32api.error: pass else: if module.__name__ == "__main__" and len(sys.argv) > 0: fname = sys.argv[0] else: fname = "" SourceCodeContainer.__init__(self, None, fname) def GetText(self): if self.text is None: fname = self.GetFileName() if fname: try: self.text = open(fname, "r").read() except OSError as details: self.text = f"# COMException opening file\n# {details!r}" else: self.text = f"# No file available for module '{self.module}'" self._buildlines() return self.text def GetName(self, dnt): name = self.module.__name__ try: fname = win32api.GetFullPathName(self.module.__file__) except win32api.error: fname = self.module.__file__ except AttributeError: fname = name if dnt == axdebug.DOCUMENTNAMETYPE_APPNODE: return name.split(".")[-1] elif dnt == axdebug.DOCUMENTNAMETYPE_TITLE: return fname elif dnt == axdebug.DOCUMENTNAMETYPE_FILE_TAIL: return os.path.split(fname)[1] elif dnt == axdebug.DOCUMENTNAMETYPE_URL: return f"file:{fname}" else: raise COMException(scode=winerror.E_UNEXPECTED) if __name__ == "__main__": from Test import ttest sc = SourceModuleContainer(ttest) # sc = SourceCodeContainer(open(sys.argv[1], "rb").read(), sys.argv[1]) attrs = sc.GetSyntaxColorAttributes() attrlen = 0 for attr in attrs: if isinstance(attr, tuple): attrlen += attr[1] else: attrlen += 1 text = sc.GetText() if attrlen != len(text): print(f"Lengths don't match!!! ({attrlen}/{len(text)})") # print("Attributes:") # print(attrs) print("GetLineOfPos=", sc.GetLineOfPosition(0)) print("GetLineOfPos=", sc.GetLineOfPosition(4)) print("GetLineOfPos=", sc.GetLineOfPosition(10))