--- a/src/eric7/QScintilla/DocstringGenerator/PyDocstringGenerator.py Wed Jul 13 11:16:20 2022 +0200 +++ b/src/eric7/QScintilla/DocstringGenerator/PyDocstringGenerator.py Wed Jul 13 14:55:47 2022 +0200 @@ -10,32 +10,31 @@ import re import collections -from .BaseDocstringGenerator import ( - BaseDocstringGenerator, FunctionInfo, getIndentStr -) +from .BaseDocstringGenerator import BaseDocstringGenerator, FunctionInfo, getIndentStr class PyDocstringGenerator(BaseDocstringGenerator): """ Class implementing a docstring generator for Python. """ + def __init__(self, editor): """ Constructor - + @param editor reference to the editor widget @type Editor """ super().__init__(editor) - + self.__quote3 = '"""' self.__quote3Alternate = "'''" - + def isFunctionStart(self, text): """ Public method to test, if a text is the start of a function or method definition. - + @param text line of text to be tested @type str @return flag indicating that the given text starts a function or @@ -46,28 +45,26 @@ text = text.lstrip() if text.startswith(("def", "async def")): return True - + return False - + def hasFunctionDefinition(self, cursorPosition): """ Public method to test, if the cursor is right below a function definition. - + @param cursorPosition current cursor position (line and column) @type tuple of (int, int) @return flag indicating cursor is right below a function definition @rtype bool """ - return ( - self.__getFunctionDefinitionFromBelow(cursorPosition) is not None - ) - + return self.__getFunctionDefinitionFromBelow(cursorPosition) is not None + def isDocstringIntro(self, cursorPosition): """ Public function to test, if the line up to the cursor position might be introducing a docstring. - + @param cursorPosition current cursor position (line and column) @type tuple of (int, int) @return flag indicating a potential start of a docstring @@ -76,12 +73,12 @@ cline, cindex = cursorPosition lineToCursor = self.editor.text(cline)[:cindex] return self.__isTripleQuotesStart(lineToCursor) - + def __isTripleQuotesStart(self, text): """ Private method to test, if the given text is the start of a triple quoted string. - + @param text text to be inspected @type str @return flag indicating a triple quote start @@ -92,12 +89,12 @@ return True return False - + def insertDocstring(self, cursorPosition, fromStart=True): """ Public method to insert a docstring for the function at the cursor position. - + @param cursorPosition position of the cursor (line and index) @type tuple of (int, int) @param fromStart flag indicating that the editor text cursor is placed @@ -106,45 +103,42 @@ """ if fromStart: self.__functionStartLine = cursorPosition[0] - docstring, insertPos, newCursorLine = ( - self.__generateDocstringFromStart() - ) + docstring, insertPos, newCursorLine = self.__generateDocstringFromStart() else: - docstring, insertPos, newCursorLine = ( - self.__generateDocstringFromBelow(cursorPosition) + docstring, insertPos, newCursorLine = self.__generateDocstringFromBelow( + cursorPosition ) - + if docstring: self.editor.beginUndoAction() self.editor.insertAt(docstring, *insertPos) - + if not fromStart: # correct triple quote indentation if neccessary - functionIndent = self.editor.indentation( - self.__functionStartLine) + functionIndent = self.editor.indentation(self.__functionStartLine) quoteIndent = self.editor.indentation(insertPos[0]) - + # step 1: unindent quote line until indentation is zero while quoteIndent > 0: self.editor.unindent(insertPos[0]) quoteIndent = self.editor.indentation(insertPos[0]) - + # step 2: indent quote line until indentation is one greater # than function definition line while quoteIndent <= functionIndent: self.editor.indent(insertPos[0]) quoteIndent = self.editor.indentation(insertPos[0]) - + self.editor.endUndoAction() self.editor.setCursorPosition( newCursorLine, len(self.editor.text(newCursorLine)) - 1 ) - + def insertDocstringFromShortcut(self, cursorPosition): """ Public method to insert a docstring for the function at the cursor position initiated via a keyboard shortcut. - + @param cursorPosition position of the cursor (line and index) @type tuple of (int, int) """ @@ -162,10 +156,8 @@ # neither after the function definition nor at the start # just do nothing return - - docstring, insertPos, newCursorLine = ( - self.__generateDocstringFromStart() - ) + + docstring, insertPos, newCursorLine = self.__generateDocstringFromStart() if docstring: self.editor.beginUndoAction() self.editor.insertAt(docstring, *insertPos) @@ -173,11 +165,11 @@ self.editor.setCursorPosition( newCursorLine, len(self.editor.text(newCursorLine)) - 1 ) - + def __getIndentationInsertString(self, text): """ Private method to create the indentation string for the docstring. - + @param text text to based the indentation on @type str @return indentation string for docstring @@ -187,19 +179,19 @@ indentWidth = self.editor.indentationWidth() if indentWidth == 0: indentWidth = self.editor.tabWidth() - + return indent + indentWidth * " " - + ####################################################################### ## Methods to generate the docstring when the text cursor is on the ## line starting the function definition. ####################################################################### - + def __generateDocstringFromStart(self): """ Private method to generate a docstring based on the cursor being placed on the first line of the definition. - + @return tuple containing the docstring and a tuple containing the insertion line and index @rtype tuple of (str, tuple(int, int)) @@ -207,15 +199,13 @@ result = self.__getFunctionDefinitionFromStart() if result: functionDefinition, functionDefinitionLength = result - + insertLine = self.__functionStartLine + functionDefinitionLength indentation = self.__getIndentationInsertString(functionDefinition) sep = self.editor.getLineSeparator() bodyStart = insertLine - - docstringList = self.__generateDocstring( - '"', functionDefinition, bodyStart - ) + + docstringList = self.__generateDocstring('"', functionDefinition, bodyStart) if docstringList: if self.getDocstringType() == "ericdoc": docstringList.insert(0, self.__quote3) @@ -225,104 +215,99 @@ newCursorLine = insertLine docstringList.append(self.__quote3) return ( - indentation + - "{0}{1}".format(sep, indentation).join(docstringList) + - sep - ), (insertLine, 0), newCursorLine - + ( + indentation + + "{0}{1}".format(sep, indentation).join(docstringList) + + sep + ), + (insertLine, 0), + newCursorLine, + ) + return "", (0, 0), 0 - + def __getFunctionDefinitionFromStart(self): """ Private method to extract the function definition based on the cursor being placed on the first line of the definition. - + @return text containing the function definition @rtype str """ startLine = self.__functionStartLine endLine = startLine + min( - self.editor.lines() - startLine, - 20 # max. 20 lines of definition allowed + self.editor.lines() - startLine, 20 # max. 20 lines of definition allowed ) isFirstLine = True functionIndent = "" functionTextList = [] - + for lineNo in range(startLine, endLine): text = self.editor.text(lineNo).rstrip() if isFirstLine: if not self.isFunctionStart(text): return None - + functionIndent = getIndentStr(text) isFirstLine = False else: currentIndent = getIndentStr(text) - if ( - currentIndent <= functionIndent or - self.isFunctionStart(text) - ): + if currentIndent <= functionIndent or self.isFunctionStart(text): # no function body exists return None if text.strip() == "": # empty line, illegal/incomplete function definition return None - + if text.endswith("\\"): text = text[:-1] - + functionTextList.append(text) - + if text.endswith(":"): # end of function definition reached functionDefinitionLength = len(functionTextList) - + # check, if function is decorated with a supported one if startLine > 0: decoratorLine = self.editor.text(startLine - 1) if ( - "@classmethod" in decoratorLine or - "@staticmethod" in decoratorLine or - "pyqtSlot" in decoratorLine or # PyQt slot - "Slot" in decoratorLine # PySide slot + "@classmethod" in decoratorLine + or "@staticmethod" in decoratorLine + or "pyqtSlot" in decoratorLine + or "Slot" in decoratorLine # PyQt slot # PySide slot ): functionTextList.insert(0, decoratorLine) - + return "".join(functionTextList), functionDefinitionLength - + return None - + ####################################################################### ## Methods to generate the docstring when the text cursor is on the ## line after the function definition (e.g. after a triple quote). ####################################################################### - + def __generateDocstringFromBelow(self, cursorPosition): """ Private method to generate a docstring when the gicen position is on the line below the end of the definition. - + @param cursorPosition position of the cursor (line and index) @type tuple of (int, int) @return tuple containing the docstring and a tuple containing the insertion line and index @rtype tuple of (str, tuple(int, int)) """ - functionDefinition = self.__getFunctionDefinitionFromBelow( - cursorPosition) + functionDefinition = self.__getFunctionDefinitionFromBelow(cursorPosition) if functionDefinition: - lineTextToCursor = ( - self.editor.text(cursorPosition[0])[:cursorPosition[1]] - ) + lineTextToCursor = self.editor.text(cursorPosition[0])[: cursorPosition[1]] insertLine = cursorPosition[0] indentation = self.__getIndentationInsertString(functionDefinition) sep = self.editor.getLineSeparator() bodyStart = insertLine - - docstringList = self.__generateDocstring( - '"', functionDefinition, bodyStart - ) + + docstringList = self.__generateDocstring('"', functionDefinition, bodyStart) if docstringList: if self.__isTripleQuotesStart(lineTextToCursor): if self.getDocstringType() == "ericdoc": @@ -339,18 +324,16 @@ docstringList[0] = self.__quote3 + docstringList[0] newCursorLine = cursorPosition[0] docstringList.append(self.__quote3) - docstring = ( - "{0}{1}".format(sep, indentation).join(docstringList) - ) + docstring = "{0}{1}".format(sep, indentation).join(docstringList) return docstring, cursorPosition, newCursorLine - + return "", (0, 0), 0 - + def __getFunctionDefinitionFromBelow(self, cursorPosition): """ Private method to extract the function definition based on the cursor being placed on the first line after the definition. - + @param cursorPosition current cursor position (line and column) @type tuple of (int, int) @return text containing the function definition @@ -361,7 +344,7 @@ # max. 20 lines of definition allowed isFirstLine = True functionTextList = [] - + for lineNo in range(startLine, endLine, -1): text = self.editor.text(lineNo).rstrip() if isFirstLine: @@ -370,39 +353,39 @@ isFirstLine = False elif text.endswith(":") or text == "": return None - + if text.endswith("\\"): text = text[:-1] - + functionTextList.insert(0, text) - + if self.isFunctionStart(text): # start of function definition reached self.__functionStartLine = lineNo - + # check, if function is decorated with a supported one if lineNo > 0: decoratorLine = self.editor.text(lineNo - 1) if ( - "@classmethod" in decoratorLine or - "@staticmethod" in decoratorLine or - "pyqtSlot" in decoratorLine or # PyQt slot - "Slot" in decoratorLine # PySide slot + "@classmethod" in decoratorLine + or "@staticmethod" in decoratorLine + or "pyqtSlot" in decoratorLine + or "Slot" in decoratorLine # PyQt slot # PySide slot ): functionTextList.insert(0, decoratorLine) - + return "".join(functionTextList) - + return None - + ####################################################################### ## Methods to generate the docstring contents. ####################################################################### - + def __getFunctionBody(self, functionIndent, startLine): """ Private method to get the function body. - + @param functionIndent indentation string of the function definition @type str @param startLine starting line for the extraction process @@ -411,24 +394,24 @@ @rtype str """ bodyList = [] - + for line in range(startLine, self.editor.lines()): text = self.editor.text(line) textIndent = getIndentStr(text) - + if text.strip() == "": pass elif len(textIndent) <= len(functionIndent): break - + bodyList.append(text) - + return "".join(bodyList) - + def __generateDocstring(self, quote, functionDef, bodyStartLine): """ Private method to generate the list of docstring lines. - + @param quote quote string @type str @param functionDef text containing the function definition @@ -445,17 +428,18 @@ quote3replace = 3 * '"' functionInfo = PyFunctionInfo() functionInfo.parseDefinition(functionDef, quote3, quote3replace) - + if functionInfo.hasInfo: - functionBody = self.__getFunctionBody(functionInfo.functionIndent, - bodyStartLine) - + functionBody = self.__getFunctionBody( + functionInfo.functionIndent, bodyStartLine + ) + if functionBody: functionInfo.parseBody(functionBody) - + docstringType = self.getDocstringType() return self._generateDocstringList(functionInfo, docstringType) - + return [] @@ -463,17 +447,18 @@ """ Class implementing an object to extract and store function information. """ + def __init__(self): """ Constructor """ super().__init__() - + def __isCharInPairs(self, posChar, pairs): """ Private method to test, if the given character position is between pairs of brackets or quotes. - + @param posChar character position to be tested @type int @param pairs list containing pairs of positions @@ -481,13 +466,12 @@ @return flag indicating the position is in between @rtype bool """ - return any(posLeft < posChar < posRight - for (posLeft, posRight) in pairs) - + return any(posLeft < posChar < posRight for (posLeft, posRight) in pairs) + def __findQuotePosition(self, text): """ Private method to find the start and end position of pairs of quotes. - + @param text text to be parsed @type str @return list of tuple with start and end position of pairs of quotes @@ -496,7 +480,7 @@ """ pos = [] foundLeftQuote = False - + for index, character in enumerate(text): if foundLeftQuote is False: if character in ("'", '"'): @@ -507,19 +491,19 @@ if character == quote and text[index - 1] != "\\": pos.append((leftPos, index)) foundLeftQuote = False - + if foundLeftQuote: raise IndexError("No matching close quote at: {0}".format(leftPos)) - + return pos - + def __findBracketPosition(self, text, bracketLeft, bracketRight, posQuote): """ Private method to find the start and end position of pairs of brackets. https://stackoverflow.com/questions/29991917/ indices-of-matching-parentheses-in-python - + @param text text to be parsed @type str @param bracketLeft character of the left bracket @@ -536,33 +520,26 @@ """ pos = [] pstack = [] - + for index, character in enumerate(text): - if ( - character == bracketLeft and - not self.__isCharInPairs(index, posQuote) - ): + if character == bracketLeft and not self.__isCharInPairs(index, posQuote): pstack.append(index) - elif ( - character == bracketRight and - not self.__isCharInPairs(index, posQuote) + elif character == bracketRight and not self.__isCharInPairs( + index, posQuote ): if len(pstack) == 0: - raise IndexError( - "No matching closing parens at: {0}".format(index)) + raise IndexError("No matching closing parens at: {0}".format(index)) pos.append((pstack.pop(), index)) - + if len(pstack) > 0: - raise IndexError( - "No matching opening parens at: {0}".format(pstack.pop())) - + raise IndexError("No matching opening parens at: {0}".format(pstack.pop())) + return pos - - def __splitArgumentToNameTypeValue(self, argumentsList, - quote, quoteReplace): + + def __splitArgumentToNameTypeValue(self, argumentsList, quote, quoteReplace): """ Private method to split some argument text to name, type and value. - + @param argumentsList list of function argument definitions @type list of str @param quote quote string to be replaced @@ -573,32 +550,29 @@ for arg in argumentsList: hasType = False hasValue = False - + colonPosition = arg.find(":") equalPosition = arg.find("=") - + if equalPosition > -1: hasValue = True - - if ( - colonPosition > -1 and - (not hasValue or equalPosition > colonPosition) - ): + + if colonPosition > -1 and (not hasValue or equalPosition > colonPosition): # exception for def foo(arg1=":") hasType = True - + if hasValue and hasType: argName = arg[0:colonPosition].strip() - argType = arg[colonPosition + 1:equalPosition].strip() - argValue = arg[equalPosition + 1:].strip() + argType = arg[colonPosition + 1 : equalPosition].strip() + argValue = arg[equalPosition + 1 :].strip() elif not hasValue and hasType: argName = arg[0:colonPosition].strip() - argType = arg[colonPosition + 1:].strip() + argType = arg[colonPosition + 1 :].strip() argValue = None elif hasValue and not hasType: argName = arg[0:equalPosition].strip() argType = None - argValue = arg[equalPosition + 1:].strip() + argValue = arg[equalPosition + 1 :].strip() else: argName = arg.strip() argType = None @@ -606,17 +580,17 @@ if argValue and quote: # sanitize argValue with respect to quotes argValue = argValue.replace(quote, quoteReplace) - + self.argumentsList.append((argName, argType, argValue)) - + def __splitArgumentsTextToList(self, argumentsText): """ Private method to split the given arguments text into a list of arguments. - + This function uses a comma to separate arguments and ignores a comma in brackets and quotes. - + @param argumentsText text containing the list of arguments @type str @return list of individual argument texts @@ -625,46 +599,43 @@ argumentsList = [] indexFindStart = 0 indexArgStart = 0 - + try: posQuote = self.__findQuotePosition(argumentsText) - posRound = self.__findBracketPosition( - argumentsText, "(", ")", posQuote) - posCurly = self.__findBracketPosition( - argumentsText, "{", "}", posQuote) - posSquare = self.__findBracketPosition( - argumentsText, "[", "]", posQuote) + posRound = self.__findBracketPosition(argumentsText, "(", ")", posQuote) + posCurly = self.__findBracketPosition(argumentsText, "{", "}", posQuote) + posSquare = self.__findBracketPosition(argumentsText, "[", "]", posQuote) except IndexError: return None - + while True: posComma = argumentsText.find(",", indexFindStart) - + if posComma == -1: break - + indexFindStart = posComma + 1 - + if ( - self.__isCharInPairs(posComma, posRound) or - self.__isCharInPairs(posComma, posCurly) or - self.__isCharInPairs(posComma, posSquare) or - self.__isCharInPairs(posComma, posQuote) + self.__isCharInPairs(posComma, posRound) + or self.__isCharInPairs(posComma, posCurly) + or self.__isCharInPairs(posComma, posSquare) + or self.__isCharInPairs(posComma, posQuote) ): continue - + argumentsList.append(argumentsText[indexArgStart:posComma]) indexArgStart = posComma + 1 - + if indexArgStart < len(argumentsText): argumentsList.append(argumentsText[indexArgStart:]) - + return argumentsList - + def parseDefinition(self, text, quote, quoteReplace): """ Public method to parse the function definition text. - + @param text text containing the function definition @type str @param quote quote string to be replaced @@ -673,7 +644,7 @@ @type str """ self.functionIndent = getIndentStr(text) - + textList = text.splitlines() if textList[0].lstrip().startswith("@"): # first line of function definition is a decorator @@ -682,17 +653,16 @@ self.functionType = "staticmethod" elif decorator == "@classmethod": self.functionType = "classmethod" - elif ( - re.match(r"@(PyQt[456]\.)?(QtCore\.)?pyqtSlot", decorator) or - re.match(r"@(PySide[26]\.)?(QtCore\.)?Slot", decorator) + elif re.match(r"@(PyQt[456]\.)?(QtCore\.)?pyqtSlot", decorator) or re.match( + r"@(PySide[26]\.)?(QtCore\.)?Slot", decorator ): self.functionType = "qtslot" - + text = "".join(textList).strip() - + if text.startswith("async def "): self.isAsync = True - + returnType = re.search(r"->[ ]*([a-zA-Z0-9_,()\[\] ]*):$", text) if returnType: self.returnTypeAnnotated = returnType.group(1) @@ -700,21 +670,19 @@ else: self.returnTypeAnnotated = None textEnd = len(text) - + positionArgumentsStart = text.find("(") + 1 - positionArgumentsEnd = text.rfind(")", positionArgumentsStart, - textEnd) - + positionArgumentsEnd = text.rfind(")", positionArgumentsStart, textEnd) + self.argumentsText = text[positionArgumentsStart:positionArgumentsEnd] - + argumentsList = self.__splitArgumentsTextToList(self.argumentsText) if argumentsList is not None: self.hasInfo = True - self.__splitArgumentToNameTypeValue( - argumentsList, quote, quoteReplace) - + self.__splitArgumentToNameTypeValue(argumentsList, quote, quoteReplace) + functionName = ( - text[:positionArgumentsStart - 1] + text[: positionArgumentsStart - 1] .replace("async def ", "") .replace("def ", "") ) @@ -729,11 +697,11 @@ self.visibility = "protected" else: self.visibility = "public" - + def parseBody(self, text): """ Public method to parse the function body text. - + @param text function body text @type str """ @@ -741,8 +709,7 @@ if len(raiseRe) > 0: self.raiseList = [x.strip() for x in raiseRe] # remove duplicates from list while keeping it in the order - self.raiseList = list(collections.OrderedDict.fromkeys( - self.raiseList)) + self.raiseList = list(collections.OrderedDict.fromkeys(self.raiseList)) yieldRe = re.search(r"[ \t]yield ", text) if yieldRe: @@ -757,10 +724,7 @@ for line in lineList: line = line.strip() - if ( - returnFound is False and - re.match(returnPattern, line) - ): + if returnFound is False and re.match(returnPattern, line): returnFound = True if returnFound: @@ -773,12 +737,9 @@ returnTmpLine = returnTmpLine[:-1] continue - self.__findBracketPosition( - returnTmpLine, "(", ")", quotePos) - self.__findBracketPosition( - returnTmpLine, "{", "}", quotePos) - self.__findBracketPosition( - returnTmpLine, "[", "]", quotePos) + self.__findBracketPosition(returnTmpLine, "(", ")", quotePos) + self.__findBracketPosition(returnTmpLine, "{", "}", quotePos) + self.__findBracketPosition(returnTmpLine, "[", "]", quotePos) except IndexError: continue