8 """ |
8 """ |
9 |
9 |
10 import re |
10 import re |
11 import collections |
11 import collections |
12 |
12 |
13 from .BaseDocstringGenerator import ( |
13 from .BaseDocstringGenerator import BaseDocstringGenerator, FunctionInfo, getIndentStr |
14 BaseDocstringGenerator, FunctionInfo, getIndentStr |
|
15 ) |
|
16 |
14 |
17 |
15 |
18 class PyDocstringGenerator(BaseDocstringGenerator): |
16 class PyDocstringGenerator(BaseDocstringGenerator): |
19 """ |
17 """ |
20 Class implementing a docstring generator for Python. |
18 Class implementing a docstring generator for Python. |
21 """ |
19 """ |
|
20 |
22 def __init__(self, editor): |
21 def __init__(self, editor): |
23 """ |
22 """ |
24 Constructor |
23 Constructor |
25 |
24 |
26 @param editor reference to the editor widget |
25 @param editor reference to the editor widget |
27 @type Editor |
26 @type Editor |
28 """ |
27 """ |
29 super().__init__(editor) |
28 super().__init__(editor) |
30 |
29 |
31 self.__quote3 = '"""' |
30 self.__quote3 = '"""' |
32 self.__quote3Alternate = "'''" |
31 self.__quote3Alternate = "'''" |
33 |
32 |
34 def isFunctionStart(self, text): |
33 def isFunctionStart(self, text): |
35 """ |
34 """ |
36 Public method to test, if a text is the start of a function or method |
35 Public method to test, if a text is the start of a function or method |
37 definition. |
36 definition. |
38 |
37 |
39 @param text line of text to be tested |
38 @param text line of text to be tested |
40 @type str |
39 @type str |
41 @return flag indicating that the given text starts a function or |
40 @return flag indicating that the given text starts a function or |
42 method definition |
41 method definition |
43 @rtype bool |
42 @rtype bool |
44 """ |
43 """ |
45 if isinstance(text, str): |
44 if isinstance(text, str): |
46 text = text.lstrip() |
45 text = text.lstrip() |
47 if text.startswith(("def", "async def")): |
46 if text.startswith(("def", "async def")): |
48 return True |
47 return True |
49 |
48 |
50 return False |
49 return False |
51 |
50 |
52 def hasFunctionDefinition(self, cursorPosition): |
51 def hasFunctionDefinition(self, cursorPosition): |
53 """ |
52 """ |
54 Public method to test, if the cursor is right below a function |
53 Public method to test, if the cursor is right below a function |
55 definition. |
54 definition. |
56 |
55 |
57 @param cursorPosition current cursor position (line and column) |
56 @param cursorPosition current cursor position (line and column) |
58 @type tuple of (int, int) |
57 @type tuple of (int, int) |
59 @return flag indicating cursor is right below a function definition |
58 @return flag indicating cursor is right below a function definition |
60 @rtype bool |
59 @rtype bool |
61 """ |
60 """ |
62 return ( |
61 return self.__getFunctionDefinitionFromBelow(cursorPosition) is not None |
63 self.__getFunctionDefinitionFromBelow(cursorPosition) is not None |
62 |
64 ) |
|
65 |
|
66 def isDocstringIntro(self, cursorPosition): |
63 def isDocstringIntro(self, cursorPosition): |
67 """ |
64 """ |
68 Public function to test, if the line up to the cursor position might be |
65 Public function to test, if the line up to the cursor position might be |
69 introducing a docstring. |
66 introducing a docstring. |
70 |
67 |
71 @param cursorPosition current cursor position (line and column) |
68 @param cursorPosition current cursor position (line and column) |
72 @type tuple of (int, int) |
69 @type tuple of (int, int) |
73 @return flag indicating a potential start of a docstring |
70 @return flag indicating a potential start of a docstring |
74 @rtype bool |
71 @rtype bool |
75 """ |
72 """ |
76 cline, cindex = cursorPosition |
73 cline, cindex = cursorPosition |
77 lineToCursor = self.editor.text(cline)[:cindex] |
74 lineToCursor = self.editor.text(cline)[:cindex] |
78 return self.__isTripleQuotesStart(lineToCursor) |
75 return self.__isTripleQuotesStart(lineToCursor) |
79 |
76 |
80 def __isTripleQuotesStart(self, text): |
77 def __isTripleQuotesStart(self, text): |
81 """ |
78 """ |
82 Private method to test, if the given text is the start of a triple |
79 Private method to test, if the given text is the start of a triple |
83 quoted string. |
80 quoted string. |
84 |
81 |
85 @param text text to be inspected |
82 @param text text to be inspected |
86 @type str |
83 @type str |
87 @return flag indicating a triple quote start |
84 @return flag indicating a triple quote start |
88 @rtype bool |
85 @rtype bool |
89 """ |
86 """ |
90 docstringTriggers = ('"""', 'r"""', "'''", "r'''") |
87 docstringTriggers = ('"""', 'r"""', "'''", "r'''") |
91 if text.lstrip() in docstringTriggers: |
88 if text.lstrip() in docstringTriggers: |
92 return True |
89 return True |
93 |
90 |
94 return False |
91 return False |
95 |
92 |
96 def insertDocstring(self, cursorPosition, fromStart=True): |
93 def insertDocstring(self, cursorPosition, fromStart=True): |
97 """ |
94 """ |
98 Public method to insert a docstring for the function at the cursor |
95 Public method to insert a docstring for the function at the cursor |
99 position. |
96 position. |
100 |
97 |
101 @param cursorPosition position of the cursor (line and index) |
98 @param cursorPosition position of the cursor (line and index) |
102 @type tuple of (int, int) |
99 @type tuple of (int, int) |
103 @param fromStart flag indicating that the editor text cursor is placed |
100 @param fromStart flag indicating that the editor text cursor is placed |
104 on the line starting the function definition |
101 on the line starting the function definition |
105 @type bool |
102 @type bool |
106 """ |
103 """ |
107 if fromStart: |
104 if fromStart: |
108 self.__functionStartLine = cursorPosition[0] |
105 self.__functionStartLine = cursorPosition[0] |
109 docstring, insertPos, newCursorLine = ( |
106 docstring, insertPos, newCursorLine = self.__generateDocstringFromStart() |
110 self.__generateDocstringFromStart() |
107 else: |
|
108 docstring, insertPos, newCursorLine = self.__generateDocstringFromBelow( |
|
109 cursorPosition |
111 ) |
110 ) |
112 else: |
111 |
113 docstring, insertPos, newCursorLine = ( |
|
114 self.__generateDocstringFromBelow(cursorPosition) |
|
115 ) |
|
116 |
|
117 if docstring: |
112 if docstring: |
118 self.editor.beginUndoAction() |
113 self.editor.beginUndoAction() |
119 self.editor.insertAt(docstring, *insertPos) |
114 self.editor.insertAt(docstring, *insertPos) |
120 |
115 |
121 if not fromStart: |
116 if not fromStart: |
122 # correct triple quote indentation if neccessary |
117 # correct triple quote indentation if neccessary |
123 functionIndent = self.editor.indentation( |
118 functionIndent = self.editor.indentation(self.__functionStartLine) |
124 self.__functionStartLine) |
|
125 quoteIndent = self.editor.indentation(insertPos[0]) |
119 quoteIndent = self.editor.indentation(insertPos[0]) |
126 |
120 |
127 # step 1: unindent quote line until indentation is zero |
121 # step 1: unindent quote line until indentation is zero |
128 while quoteIndent > 0: |
122 while quoteIndent > 0: |
129 self.editor.unindent(insertPos[0]) |
123 self.editor.unindent(insertPos[0]) |
130 quoteIndent = self.editor.indentation(insertPos[0]) |
124 quoteIndent = self.editor.indentation(insertPos[0]) |
131 |
125 |
132 # step 2: indent quote line until indentation is one greater |
126 # step 2: indent quote line until indentation is one greater |
133 # than function definition line |
127 # than function definition line |
134 while quoteIndent <= functionIndent: |
128 while quoteIndent <= functionIndent: |
135 self.editor.indent(insertPos[0]) |
129 self.editor.indent(insertPos[0]) |
136 quoteIndent = self.editor.indentation(insertPos[0]) |
130 quoteIndent = self.editor.indentation(insertPos[0]) |
137 |
131 |
138 self.editor.endUndoAction() |
132 self.editor.endUndoAction() |
139 self.editor.setCursorPosition( |
133 self.editor.setCursorPosition( |
140 newCursorLine, len(self.editor.text(newCursorLine)) - 1 |
134 newCursorLine, len(self.editor.text(newCursorLine)) - 1 |
141 ) |
135 ) |
142 |
136 |
143 def insertDocstringFromShortcut(self, cursorPosition): |
137 def insertDocstringFromShortcut(self, cursorPosition): |
144 """ |
138 """ |
145 Public method to insert a docstring for the function at the cursor |
139 Public method to insert a docstring for the function at the cursor |
146 position initiated via a keyboard shortcut. |
140 position initiated via a keyboard shortcut. |
147 |
141 |
148 @param cursorPosition position of the cursor (line and index) |
142 @param cursorPosition position of the cursor (line and index) |
149 @type tuple of (int, int) |
143 @type tuple of (int, int) |
150 """ |
144 """ |
151 result = self.__getFunctionDefinitionFromBelow(cursorPosition) |
145 result = self.__getFunctionDefinitionFromBelow(cursorPosition) |
152 if result is not None: |
146 if result is not None: |
160 self.__functionStartLine = cursorPosition[0] |
154 self.__functionStartLine = cursorPosition[0] |
161 else: |
155 else: |
162 # neither after the function definition nor at the start |
156 # neither after the function definition nor at the start |
163 # just do nothing |
157 # just do nothing |
164 return |
158 return |
165 |
159 |
166 docstring, insertPos, newCursorLine = ( |
160 docstring, insertPos, newCursorLine = self.__generateDocstringFromStart() |
167 self.__generateDocstringFromStart() |
|
168 ) |
|
169 if docstring: |
161 if docstring: |
170 self.editor.beginUndoAction() |
162 self.editor.beginUndoAction() |
171 self.editor.insertAt(docstring, *insertPos) |
163 self.editor.insertAt(docstring, *insertPos) |
172 self.editor.endUndoAction() |
164 self.editor.endUndoAction() |
173 self.editor.setCursorPosition( |
165 self.editor.setCursorPosition( |
174 newCursorLine, len(self.editor.text(newCursorLine)) - 1 |
166 newCursorLine, len(self.editor.text(newCursorLine)) - 1 |
175 ) |
167 ) |
176 |
168 |
177 def __getIndentationInsertString(self, text): |
169 def __getIndentationInsertString(self, text): |
178 """ |
170 """ |
179 Private method to create the indentation string for the docstring. |
171 Private method to create the indentation string for the docstring. |
180 |
172 |
181 @param text text to based the indentation on |
173 @param text text to based the indentation on |
182 @type str |
174 @type str |
183 @return indentation string for docstring |
175 @return indentation string for docstring |
184 @rtype str |
176 @rtype str |
185 """ |
177 """ |
186 indent = getIndentStr(text) |
178 indent = getIndentStr(text) |
187 indentWidth = self.editor.indentationWidth() |
179 indentWidth = self.editor.indentationWidth() |
188 if indentWidth == 0: |
180 if indentWidth == 0: |
189 indentWidth = self.editor.tabWidth() |
181 indentWidth = self.editor.tabWidth() |
190 |
182 |
191 return indent + indentWidth * " " |
183 return indent + indentWidth * " " |
192 |
184 |
193 ####################################################################### |
185 ####################################################################### |
194 ## Methods to generate the docstring when the text cursor is on the |
186 ## Methods to generate the docstring when the text cursor is on the |
195 ## line starting the function definition. |
187 ## line starting the function definition. |
196 ####################################################################### |
188 ####################################################################### |
197 |
189 |
198 def __generateDocstringFromStart(self): |
190 def __generateDocstringFromStart(self): |
199 """ |
191 """ |
200 Private method to generate a docstring based on the cursor being |
192 Private method to generate a docstring based on the cursor being |
201 placed on the first line of the definition. |
193 placed on the first line of the definition. |
202 |
194 |
203 @return tuple containing the docstring and a tuple containing the |
195 @return tuple containing the docstring and a tuple containing the |
204 insertion line and index |
196 insertion line and index |
205 @rtype tuple of (str, tuple(int, int)) |
197 @rtype tuple of (str, tuple(int, int)) |
206 """ |
198 """ |
207 result = self.__getFunctionDefinitionFromStart() |
199 result = self.__getFunctionDefinitionFromStart() |
208 if result: |
200 if result: |
209 functionDefinition, functionDefinitionLength = result |
201 functionDefinition, functionDefinitionLength = result |
210 |
202 |
211 insertLine = self.__functionStartLine + functionDefinitionLength |
203 insertLine = self.__functionStartLine + functionDefinitionLength |
212 indentation = self.__getIndentationInsertString(functionDefinition) |
204 indentation = self.__getIndentationInsertString(functionDefinition) |
213 sep = self.editor.getLineSeparator() |
205 sep = self.editor.getLineSeparator() |
214 bodyStart = insertLine |
206 bodyStart = insertLine |
215 |
207 |
216 docstringList = self.__generateDocstring( |
208 docstringList = self.__generateDocstring('"', functionDefinition, bodyStart) |
217 '"', functionDefinition, bodyStart |
|
218 ) |
|
219 if docstringList: |
209 if docstringList: |
220 if self.getDocstringType() == "ericdoc": |
210 if self.getDocstringType() == "ericdoc": |
221 docstringList.insert(0, self.__quote3) |
211 docstringList.insert(0, self.__quote3) |
222 newCursorLine = insertLine + 1 |
212 newCursorLine = insertLine + 1 |
223 else: |
213 else: |
224 docstringList[0] = self.__quote3 + docstringList[0] |
214 docstringList[0] = self.__quote3 + docstringList[0] |
225 newCursorLine = insertLine |
215 newCursorLine = insertLine |
226 docstringList.append(self.__quote3) |
216 docstringList.append(self.__quote3) |
227 return ( |
217 return ( |
228 indentation + |
218 ( |
229 "{0}{1}".format(sep, indentation).join(docstringList) + |
219 indentation |
230 sep |
220 + "{0}{1}".format(sep, indentation).join(docstringList) |
231 ), (insertLine, 0), newCursorLine |
221 + sep |
232 |
222 ), |
|
223 (insertLine, 0), |
|
224 newCursorLine, |
|
225 ) |
|
226 |
233 return "", (0, 0), 0 |
227 return "", (0, 0), 0 |
234 |
228 |
235 def __getFunctionDefinitionFromStart(self): |
229 def __getFunctionDefinitionFromStart(self): |
236 """ |
230 """ |
237 Private method to extract the function definition based on the cursor |
231 Private method to extract the function definition based on the cursor |
238 being placed on the first line of the definition. |
232 being placed on the first line of the definition. |
239 |
233 |
240 @return text containing the function definition |
234 @return text containing the function definition |
241 @rtype str |
235 @rtype str |
242 """ |
236 """ |
243 startLine = self.__functionStartLine |
237 startLine = self.__functionStartLine |
244 endLine = startLine + min( |
238 endLine = startLine + min( |
245 self.editor.lines() - startLine, |
239 self.editor.lines() - startLine, 20 # max. 20 lines of definition allowed |
246 20 # max. 20 lines of definition allowed |
|
247 ) |
240 ) |
248 isFirstLine = True |
241 isFirstLine = True |
249 functionIndent = "" |
242 functionIndent = "" |
250 functionTextList = [] |
243 functionTextList = [] |
251 |
244 |
252 for lineNo in range(startLine, endLine): |
245 for lineNo in range(startLine, endLine): |
253 text = self.editor.text(lineNo).rstrip() |
246 text = self.editor.text(lineNo).rstrip() |
254 if isFirstLine: |
247 if isFirstLine: |
255 if not self.isFunctionStart(text): |
248 if not self.isFunctionStart(text): |
256 return None |
249 return None |
257 |
250 |
258 functionIndent = getIndentStr(text) |
251 functionIndent = getIndentStr(text) |
259 isFirstLine = False |
252 isFirstLine = False |
260 else: |
253 else: |
261 currentIndent = getIndentStr(text) |
254 currentIndent = getIndentStr(text) |
262 if ( |
255 if currentIndent <= functionIndent or self.isFunctionStart(text): |
263 currentIndent <= functionIndent or |
|
264 self.isFunctionStart(text) |
|
265 ): |
|
266 # no function body exists |
256 # no function body exists |
267 return None |
257 return None |
268 if text.strip() == "": |
258 if text.strip() == "": |
269 # empty line, illegal/incomplete function definition |
259 # empty line, illegal/incomplete function definition |
270 return None |
260 return None |
271 |
261 |
272 if text.endswith("\\"): |
262 if text.endswith("\\"): |
273 text = text[:-1] |
263 text = text[:-1] |
274 |
264 |
275 functionTextList.append(text) |
265 functionTextList.append(text) |
276 |
266 |
277 if text.endswith(":"): |
267 if text.endswith(":"): |
278 # end of function definition reached |
268 # end of function definition reached |
279 functionDefinitionLength = len(functionTextList) |
269 functionDefinitionLength = len(functionTextList) |
280 |
270 |
281 # check, if function is decorated with a supported one |
271 # check, if function is decorated with a supported one |
282 if startLine > 0: |
272 if startLine > 0: |
283 decoratorLine = self.editor.text(startLine - 1) |
273 decoratorLine = self.editor.text(startLine - 1) |
284 if ( |
274 if ( |
285 "@classmethod" in decoratorLine or |
275 "@classmethod" in decoratorLine |
286 "@staticmethod" in decoratorLine or |
276 or "@staticmethod" in decoratorLine |
287 "pyqtSlot" in decoratorLine or # PyQt slot |
277 or "pyqtSlot" in decoratorLine |
288 "Slot" in decoratorLine # PySide slot |
278 or "Slot" in decoratorLine # PyQt slot # PySide slot |
289 ): |
279 ): |
290 functionTextList.insert(0, decoratorLine) |
280 functionTextList.insert(0, decoratorLine) |
291 |
281 |
292 return "".join(functionTextList), functionDefinitionLength |
282 return "".join(functionTextList), functionDefinitionLength |
293 |
283 |
294 return None |
284 return None |
295 |
285 |
296 ####################################################################### |
286 ####################################################################### |
297 ## Methods to generate the docstring when the text cursor is on the |
287 ## Methods to generate the docstring when the text cursor is on the |
298 ## line after the function definition (e.g. after a triple quote). |
288 ## line after the function definition (e.g. after a triple quote). |
299 ####################################################################### |
289 ####################################################################### |
300 |
290 |
301 def __generateDocstringFromBelow(self, cursorPosition): |
291 def __generateDocstringFromBelow(self, cursorPosition): |
302 """ |
292 """ |
303 Private method to generate a docstring when the gicen position is on |
293 Private method to generate a docstring when the gicen position is on |
304 the line below the end of the definition. |
294 the line below the end of the definition. |
305 |
295 |
306 @param cursorPosition position of the cursor (line and index) |
296 @param cursorPosition position of the cursor (line and index) |
307 @type tuple of (int, int) |
297 @type tuple of (int, int) |
308 @return tuple containing the docstring and a tuple containing the |
298 @return tuple containing the docstring and a tuple containing the |
309 insertion line and index |
299 insertion line and index |
310 @rtype tuple of (str, tuple(int, int)) |
300 @rtype tuple of (str, tuple(int, int)) |
311 """ |
301 """ |
312 functionDefinition = self.__getFunctionDefinitionFromBelow( |
302 functionDefinition = self.__getFunctionDefinitionFromBelow(cursorPosition) |
313 cursorPosition) |
|
314 if functionDefinition: |
303 if functionDefinition: |
315 lineTextToCursor = ( |
304 lineTextToCursor = self.editor.text(cursorPosition[0])[: cursorPosition[1]] |
316 self.editor.text(cursorPosition[0])[:cursorPosition[1]] |
|
317 ) |
|
318 insertLine = cursorPosition[0] |
305 insertLine = cursorPosition[0] |
319 indentation = self.__getIndentationInsertString(functionDefinition) |
306 indentation = self.__getIndentationInsertString(functionDefinition) |
320 sep = self.editor.getLineSeparator() |
307 sep = self.editor.getLineSeparator() |
321 bodyStart = insertLine |
308 bodyStart = insertLine |
322 |
309 |
323 docstringList = self.__generateDocstring( |
310 docstringList = self.__generateDocstring('"', functionDefinition, bodyStart) |
324 '"', functionDefinition, bodyStart |
|
325 ) |
|
326 if docstringList: |
311 if docstringList: |
327 if self.__isTripleQuotesStart(lineTextToCursor): |
312 if self.__isTripleQuotesStart(lineTextToCursor): |
328 if self.getDocstringType() == "ericdoc": |
313 if self.getDocstringType() == "ericdoc": |
329 docstringList.insert(0, "") |
314 docstringList.insert(0, "") |
330 newCursorLine = cursorPosition[0] + 1 |
315 newCursorLine = cursorPosition[0] + 1 |
337 newCursorLine = cursorPosition[0] + 1 |
322 newCursorLine = cursorPosition[0] + 1 |
338 else: |
323 else: |
339 docstringList[0] = self.__quote3 + docstringList[0] |
324 docstringList[0] = self.__quote3 + docstringList[0] |
340 newCursorLine = cursorPosition[0] |
325 newCursorLine = cursorPosition[0] |
341 docstringList.append(self.__quote3) |
326 docstringList.append(self.__quote3) |
342 docstring = ( |
327 docstring = "{0}{1}".format(sep, indentation).join(docstringList) |
343 "{0}{1}".format(sep, indentation).join(docstringList) |
|
344 ) |
|
345 return docstring, cursorPosition, newCursorLine |
328 return docstring, cursorPosition, newCursorLine |
346 |
329 |
347 return "", (0, 0), 0 |
330 return "", (0, 0), 0 |
348 |
331 |
349 def __getFunctionDefinitionFromBelow(self, cursorPosition): |
332 def __getFunctionDefinitionFromBelow(self, cursorPosition): |
350 """ |
333 """ |
351 Private method to extract the function definition based on the cursor |
334 Private method to extract the function definition based on the cursor |
352 being placed on the first line after the definition. |
335 being placed on the first line after the definition. |
353 |
336 |
354 @param cursorPosition current cursor position (line and column) |
337 @param cursorPosition current cursor position (line and column) |
355 @type tuple of (int, int) |
338 @type tuple of (int, int) |
356 @return text containing the function definition |
339 @return text containing the function definition |
357 @rtype str |
340 @rtype str |
358 """ |
341 """ |
359 startLine = cursorPosition[0] - 1 |
342 startLine = cursorPosition[0] - 1 |
360 endLine = startLine - min(startLine, 20) |
343 endLine = startLine - min(startLine, 20) |
361 # max. 20 lines of definition allowed |
344 # max. 20 lines of definition allowed |
362 isFirstLine = True |
345 isFirstLine = True |
363 functionTextList = [] |
346 functionTextList = [] |
364 |
347 |
365 for lineNo in range(startLine, endLine, -1): |
348 for lineNo in range(startLine, endLine, -1): |
366 text = self.editor.text(lineNo).rstrip() |
349 text = self.editor.text(lineNo).rstrip() |
367 if isFirstLine: |
350 if isFirstLine: |
368 if not text.endswith(":"): |
351 if not text.endswith(":"): |
369 return None |
352 return None |
370 isFirstLine = False |
353 isFirstLine = False |
371 elif text.endswith(":") or text == "": |
354 elif text.endswith(":") or text == "": |
372 return None |
355 return None |
373 |
356 |
374 if text.endswith("\\"): |
357 if text.endswith("\\"): |
375 text = text[:-1] |
358 text = text[:-1] |
376 |
359 |
377 functionTextList.insert(0, text) |
360 functionTextList.insert(0, text) |
378 |
361 |
379 if self.isFunctionStart(text): |
362 if self.isFunctionStart(text): |
380 # start of function definition reached |
363 # start of function definition reached |
381 self.__functionStartLine = lineNo |
364 self.__functionStartLine = lineNo |
382 |
365 |
383 # check, if function is decorated with a supported one |
366 # check, if function is decorated with a supported one |
384 if lineNo > 0: |
367 if lineNo > 0: |
385 decoratorLine = self.editor.text(lineNo - 1) |
368 decoratorLine = self.editor.text(lineNo - 1) |
386 if ( |
369 if ( |
387 "@classmethod" in decoratorLine or |
370 "@classmethod" in decoratorLine |
388 "@staticmethod" in decoratorLine or |
371 or "@staticmethod" in decoratorLine |
389 "pyqtSlot" in decoratorLine or # PyQt slot |
372 or "pyqtSlot" in decoratorLine |
390 "Slot" in decoratorLine # PySide slot |
373 or "Slot" in decoratorLine # PyQt slot # PySide slot |
391 ): |
374 ): |
392 functionTextList.insert(0, decoratorLine) |
375 functionTextList.insert(0, decoratorLine) |
393 |
376 |
394 return "".join(functionTextList) |
377 return "".join(functionTextList) |
395 |
378 |
396 return None |
379 return None |
397 |
380 |
398 ####################################################################### |
381 ####################################################################### |
399 ## Methods to generate the docstring contents. |
382 ## Methods to generate the docstring contents. |
400 ####################################################################### |
383 ####################################################################### |
401 |
384 |
402 def __getFunctionBody(self, functionIndent, startLine): |
385 def __getFunctionBody(self, functionIndent, startLine): |
403 """ |
386 """ |
404 Private method to get the function body. |
387 Private method to get the function body. |
405 |
388 |
406 @param functionIndent indentation string of the function definition |
389 @param functionIndent indentation string of the function definition |
407 @type str |
390 @type str |
408 @param startLine starting line for the extraction process |
391 @param startLine starting line for the extraction process |
409 @type int |
392 @type int |
410 @return text containing the function body |
393 @return text containing the function body |
411 @rtype str |
394 @rtype str |
412 """ |
395 """ |
413 bodyList = [] |
396 bodyList = [] |
414 |
397 |
415 for line in range(startLine, self.editor.lines()): |
398 for line in range(startLine, self.editor.lines()): |
416 text = self.editor.text(line) |
399 text = self.editor.text(line) |
417 textIndent = getIndentStr(text) |
400 textIndent = getIndentStr(text) |
418 |
401 |
419 if text.strip() == "": |
402 if text.strip() == "": |
420 pass |
403 pass |
421 elif len(textIndent) <= len(functionIndent): |
404 elif len(textIndent) <= len(functionIndent): |
422 break |
405 break |
423 |
406 |
424 bodyList.append(text) |
407 bodyList.append(text) |
425 |
408 |
426 return "".join(bodyList) |
409 return "".join(bodyList) |
427 |
410 |
428 def __generateDocstring(self, quote, functionDef, bodyStartLine): |
411 def __generateDocstring(self, quote, functionDef, bodyStartLine): |
429 """ |
412 """ |
430 Private method to generate the list of docstring lines. |
413 Private method to generate the list of docstring lines. |
431 |
414 |
432 @param quote quote string |
415 @param quote quote string |
433 @type str |
416 @type str |
434 @param functionDef text containing the function definition |
417 @param functionDef text containing the function definition |
435 @type str |
418 @type str |
436 @param bodyStartLine starting line of the function body |
419 @param bodyStartLine starting line of the function body |
443 quote3replace = 3 * "'" |
426 quote3replace = 3 * "'" |
444 elif quote == "'": |
427 elif quote == "'": |
445 quote3replace = 3 * '"' |
428 quote3replace = 3 * '"' |
446 functionInfo = PyFunctionInfo() |
429 functionInfo = PyFunctionInfo() |
447 functionInfo.parseDefinition(functionDef, quote3, quote3replace) |
430 functionInfo.parseDefinition(functionDef, quote3, quote3replace) |
448 |
431 |
449 if functionInfo.hasInfo: |
432 if functionInfo.hasInfo: |
450 functionBody = self.__getFunctionBody(functionInfo.functionIndent, |
433 functionBody = self.__getFunctionBody( |
451 bodyStartLine) |
434 functionInfo.functionIndent, bodyStartLine |
452 |
435 ) |
|
436 |
453 if functionBody: |
437 if functionBody: |
454 functionInfo.parseBody(functionBody) |
438 functionInfo.parseBody(functionBody) |
455 |
439 |
456 docstringType = self.getDocstringType() |
440 docstringType = self.getDocstringType() |
457 return self._generateDocstringList(functionInfo, docstringType) |
441 return self._generateDocstringList(functionInfo, docstringType) |
458 |
442 |
459 return [] |
443 return [] |
460 |
444 |
461 |
445 |
462 class PyFunctionInfo(FunctionInfo): |
446 class PyFunctionInfo(FunctionInfo): |
463 """ |
447 """ |
464 Class implementing an object to extract and store function information. |
448 Class implementing an object to extract and store function information. |
465 """ |
449 """ |
|
450 |
466 def __init__(self): |
451 def __init__(self): |
467 """ |
452 """ |
468 Constructor |
453 Constructor |
469 """ |
454 """ |
470 super().__init__() |
455 super().__init__() |
471 |
456 |
472 def __isCharInPairs(self, posChar, pairs): |
457 def __isCharInPairs(self, posChar, pairs): |
473 """ |
458 """ |
474 Private method to test, if the given character position is between |
459 Private method to test, if the given character position is between |
475 pairs of brackets or quotes. |
460 pairs of brackets or quotes. |
476 |
461 |
477 @param posChar character position to be tested |
462 @param posChar character position to be tested |
478 @type int |
463 @type int |
479 @param pairs list containing pairs of positions |
464 @param pairs list containing pairs of positions |
480 @type list of tuple of (int, int) |
465 @type list of tuple of (int, int) |
481 @return flag indicating the position is in between |
466 @return flag indicating the position is in between |
482 @rtype bool |
467 @rtype bool |
483 """ |
468 """ |
484 return any(posLeft < posChar < posRight |
469 return any(posLeft < posChar < posRight for (posLeft, posRight) in pairs) |
485 for (posLeft, posRight) in pairs) |
470 |
486 |
|
487 def __findQuotePosition(self, text): |
471 def __findQuotePosition(self, text): |
488 """ |
472 """ |
489 Private method to find the start and end position of pairs of quotes. |
473 Private method to find the start and end position of pairs of quotes. |
490 |
474 |
491 @param text text to be parsed |
475 @param text text to be parsed |
492 @type str |
476 @type str |
493 @return list of tuple with start and end position of pairs of quotes |
477 @return list of tuple with start and end position of pairs of quotes |
494 @rtype list of tuple of (int, int) |
478 @rtype list of tuple of (int, int) |
495 @exception IndexError raised when a matching close quote is missing |
479 @exception IndexError raised when a matching close quote is missing |
496 """ |
480 """ |
497 pos = [] |
481 pos = [] |
498 foundLeftQuote = False |
482 foundLeftQuote = False |
499 |
483 |
500 for index, character in enumerate(text): |
484 for index, character in enumerate(text): |
501 if foundLeftQuote is False: |
485 if foundLeftQuote is False: |
502 if character in ("'", '"'): |
486 if character in ("'", '"'): |
503 foundLeftQuote = True |
487 foundLeftQuote = True |
504 quote = character |
488 quote = character |
505 leftPos = index |
489 leftPos = index |
506 else: |
490 else: |
507 if character == quote and text[index - 1] != "\\": |
491 if character == quote and text[index - 1] != "\\": |
508 pos.append((leftPos, index)) |
492 pos.append((leftPos, index)) |
509 foundLeftQuote = False |
493 foundLeftQuote = False |
510 |
494 |
511 if foundLeftQuote: |
495 if foundLeftQuote: |
512 raise IndexError("No matching close quote at: {0}".format(leftPos)) |
496 raise IndexError("No matching close quote at: {0}".format(leftPos)) |
513 |
497 |
514 return pos |
498 return pos |
515 |
499 |
516 def __findBracketPosition(self, text, bracketLeft, bracketRight, posQuote): |
500 def __findBracketPosition(self, text, bracketLeft, bracketRight, posQuote): |
517 """ |
501 """ |
518 Private method to find the start and end position of pairs of brackets. |
502 Private method to find the start and end position of pairs of brackets. |
519 |
503 |
520 https://stackoverflow.com/questions/29991917/ |
504 https://stackoverflow.com/questions/29991917/ |
521 indices-of-matching-parentheses-in-python |
505 indices-of-matching-parentheses-in-python |
522 |
506 |
523 @param text text to be parsed |
507 @param text text to be parsed |
524 @type str |
508 @type str |
525 @param bracketLeft character of the left bracket |
509 @param bracketLeft character of the left bracket |
526 @type str |
510 @type str |
527 @param bracketRight character of the right bracket |
511 @param bracketRight character of the right bracket |
534 @exception IndexError raised when a closing or opening bracket is |
518 @exception IndexError raised when a closing or opening bracket is |
535 missing |
519 missing |
536 """ |
520 """ |
537 pos = [] |
521 pos = [] |
538 pstack = [] |
522 pstack = [] |
539 |
523 |
540 for index, character in enumerate(text): |
524 for index, character in enumerate(text): |
541 if ( |
525 if character == bracketLeft and not self.__isCharInPairs(index, posQuote): |
542 character == bracketLeft and |
|
543 not self.__isCharInPairs(index, posQuote) |
|
544 ): |
|
545 pstack.append(index) |
526 pstack.append(index) |
546 elif ( |
527 elif character == bracketRight and not self.__isCharInPairs( |
547 character == bracketRight and |
528 index, posQuote |
548 not self.__isCharInPairs(index, posQuote) |
|
549 ): |
529 ): |
550 if len(pstack) == 0: |
530 if len(pstack) == 0: |
551 raise IndexError( |
531 raise IndexError("No matching closing parens at: {0}".format(index)) |
552 "No matching closing parens at: {0}".format(index)) |
|
553 pos.append((pstack.pop(), index)) |
532 pos.append((pstack.pop(), index)) |
554 |
533 |
555 if len(pstack) > 0: |
534 if len(pstack) > 0: |
556 raise IndexError( |
535 raise IndexError("No matching opening parens at: {0}".format(pstack.pop())) |
557 "No matching opening parens at: {0}".format(pstack.pop())) |
536 |
558 |
|
559 return pos |
537 return pos |
560 |
538 |
561 def __splitArgumentToNameTypeValue(self, argumentsList, |
539 def __splitArgumentToNameTypeValue(self, argumentsList, quote, quoteReplace): |
562 quote, quoteReplace): |
|
563 """ |
540 """ |
564 Private method to split some argument text to name, type and value. |
541 Private method to split some argument text to name, type and value. |
565 |
542 |
566 @param argumentsList list of function argument definitions |
543 @param argumentsList list of function argument definitions |
567 @type list of str |
544 @type list of str |
568 @param quote quote string to be replaced |
545 @param quote quote string to be replaced |
569 @type str |
546 @type str |
570 @param quoteReplace quote string to replace the original |
547 @param quoteReplace quote string to replace the original |
571 @type str |
548 @type str |
572 """ |
549 """ |
573 for arg in argumentsList: |
550 for arg in argumentsList: |
574 hasType = False |
551 hasType = False |
575 hasValue = False |
552 hasValue = False |
576 |
553 |
577 colonPosition = arg.find(":") |
554 colonPosition = arg.find(":") |
578 equalPosition = arg.find("=") |
555 equalPosition = arg.find("=") |
579 |
556 |
580 if equalPosition > -1: |
557 if equalPosition > -1: |
581 hasValue = True |
558 hasValue = True |
582 |
559 |
583 if ( |
560 if colonPosition > -1 and (not hasValue or equalPosition > colonPosition): |
584 colonPosition > -1 and |
|
585 (not hasValue or equalPosition > colonPosition) |
|
586 ): |
|
587 # exception for def foo(arg1=":") |
561 # exception for def foo(arg1=":") |
588 hasType = True |
562 hasType = True |
589 |
563 |
590 if hasValue and hasType: |
564 if hasValue and hasType: |
591 argName = arg[0:colonPosition].strip() |
565 argName = arg[0:colonPosition].strip() |
592 argType = arg[colonPosition + 1:equalPosition].strip() |
566 argType = arg[colonPosition + 1 : equalPosition].strip() |
593 argValue = arg[equalPosition + 1:].strip() |
567 argValue = arg[equalPosition + 1 :].strip() |
594 elif not hasValue and hasType: |
568 elif not hasValue and hasType: |
595 argName = arg[0:colonPosition].strip() |
569 argName = arg[0:colonPosition].strip() |
596 argType = arg[colonPosition + 1:].strip() |
570 argType = arg[colonPosition + 1 :].strip() |
597 argValue = None |
571 argValue = None |
598 elif hasValue and not hasType: |
572 elif hasValue and not hasType: |
599 argName = arg[0:equalPosition].strip() |
573 argName = arg[0:equalPosition].strip() |
600 argType = None |
574 argType = None |
601 argValue = arg[equalPosition + 1:].strip() |
575 argValue = arg[equalPosition + 1 :].strip() |
602 else: |
576 else: |
603 argName = arg.strip() |
577 argName = arg.strip() |
604 argType = None |
578 argType = None |
605 argValue = None |
579 argValue = None |
606 if argValue and quote: |
580 if argValue and quote: |
607 # sanitize argValue with respect to quotes |
581 # sanitize argValue with respect to quotes |
608 argValue = argValue.replace(quote, quoteReplace) |
582 argValue = argValue.replace(quote, quoteReplace) |
609 |
583 |
610 self.argumentsList.append((argName, argType, argValue)) |
584 self.argumentsList.append((argName, argType, argValue)) |
611 |
585 |
612 def __splitArgumentsTextToList(self, argumentsText): |
586 def __splitArgumentsTextToList(self, argumentsText): |
613 """ |
587 """ |
614 Private method to split the given arguments text into a list of |
588 Private method to split the given arguments text into a list of |
615 arguments. |
589 arguments. |
616 |
590 |
617 This function uses a comma to separate arguments and ignores a comma in |
591 This function uses a comma to separate arguments and ignores a comma in |
618 brackets and quotes. |
592 brackets and quotes. |
619 |
593 |
620 @param argumentsText text containing the list of arguments |
594 @param argumentsText text containing the list of arguments |
621 @type str |
595 @type str |
622 @return list of individual argument texts |
596 @return list of individual argument texts |
623 @rtype list of str |
597 @rtype list of str |
624 """ |
598 """ |
625 argumentsList = [] |
599 argumentsList = [] |
626 indexFindStart = 0 |
600 indexFindStart = 0 |
627 indexArgStart = 0 |
601 indexArgStart = 0 |
628 |
602 |
629 try: |
603 try: |
630 posQuote = self.__findQuotePosition(argumentsText) |
604 posQuote = self.__findQuotePosition(argumentsText) |
631 posRound = self.__findBracketPosition( |
605 posRound = self.__findBracketPosition(argumentsText, "(", ")", posQuote) |
632 argumentsText, "(", ")", posQuote) |
606 posCurly = self.__findBracketPosition(argumentsText, "{", "}", posQuote) |
633 posCurly = self.__findBracketPosition( |
607 posSquare = self.__findBracketPosition(argumentsText, "[", "]", posQuote) |
634 argumentsText, "{", "}", posQuote) |
|
635 posSquare = self.__findBracketPosition( |
|
636 argumentsText, "[", "]", posQuote) |
|
637 except IndexError: |
608 except IndexError: |
638 return None |
609 return None |
639 |
610 |
640 while True: |
611 while True: |
641 posComma = argumentsText.find(",", indexFindStart) |
612 posComma = argumentsText.find(",", indexFindStart) |
642 |
613 |
643 if posComma == -1: |
614 if posComma == -1: |
644 break |
615 break |
645 |
616 |
646 indexFindStart = posComma + 1 |
617 indexFindStart = posComma + 1 |
647 |
618 |
648 if ( |
619 if ( |
649 self.__isCharInPairs(posComma, posRound) or |
620 self.__isCharInPairs(posComma, posRound) |
650 self.__isCharInPairs(posComma, posCurly) or |
621 or self.__isCharInPairs(posComma, posCurly) |
651 self.__isCharInPairs(posComma, posSquare) or |
622 or self.__isCharInPairs(posComma, posSquare) |
652 self.__isCharInPairs(posComma, posQuote) |
623 or self.__isCharInPairs(posComma, posQuote) |
653 ): |
624 ): |
654 continue |
625 continue |
655 |
626 |
656 argumentsList.append(argumentsText[indexArgStart:posComma]) |
627 argumentsList.append(argumentsText[indexArgStart:posComma]) |
657 indexArgStart = posComma + 1 |
628 indexArgStart = posComma + 1 |
658 |
629 |
659 if indexArgStart < len(argumentsText): |
630 if indexArgStart < len(argumentsText): |
660 argumentsList.append(argumentsText[indexArgStart:]) |
631 argumentsList.append(argumentsText[indexArgStart:]) |
661 |
632 |
662 return argumentsList |
633 return argumentsList |
663 |
634 |
664 def parseDefinition(self, text, quote, quoteReplace): |
635 def parseDefinition(self, text, quote, quoteReplace): |
665 """ |
636 """ |
666 Public method to parse the function definition text. |
637 Public method to parse the function definition text. |
667 |
638 |
668 @param text text containing the function definition |
639 @param text text containing the function definition |
669 @type str |
640 @type str |
670 @param quote quote string to be replaced |
641 @param quote quote string to be replaced |
671 @type str |
642 @type str |
672 @param quoteReplace quote string to replace the original |
643 @param quoteReplace quote string to replace the original |
673 @type str |
644 @type str |
674 """ |
645 """ |
675 self.functionIndent = getIndentStr(text) |
646 self.functionIndent = getIndentStr(text) |
676 |
647 |
677 textList = text.splitlines() |
648 textList = text.splitlines() |
678 if textList[0].lstrip().startswith("@"): |
649 if textList[0].lstrip().startswith("@"): |
679 # first line of function definition is a decorator |
650 # first line of function definition is a decorator |
680 decorator = textList.pop(0).strip() |
651 decorator = textList.pop(0).strip() |
681 if decorator == "@staticmethod": |
652 if decorator == "@staticmethod": |
682 self.functionType = "staticmethod" |
653 self.functionType = "staticmethod" |
683 elif decorator == "@classmethod": |
654 elif decorator == "@classmethod": |
684 self.functionType = "classmethod" |
655 self.functionType = "classmethod" |
685 elif ( |
656 elif re.match(r"@(PyQt[456]\.)?(QtCore\.)?pyqtSlot", decorator) or re.match( |
686 re.match(r"@(PyQt[456]\.)?(QtCore\.)?pyqtSlot", decorator) or |
657 r"@(PySide[26]\.)?(QtCore\.)?Slot", decorator |
687 re.match(r"@(PySide[26]\.)?(QtCore\.)?Slot", decorator) |
|
688 ): |
658 ): |
689 self.functionType = "qtslot" |
659 self.functionType = "qtslot" |
690 |
660 |
691 text = "".join(textList).strip() |
661 text = "".join(textList).strip() |
692 |
662 |
693 if text.startswith("async def "): |
663 if text.startswith("async def "): |
694 self.isAsync = True |
664 self.isAsync = True |
695 |
665 |
696 returnType = re.search(r"->[ ]*([a-zA-Z0-9_,()\[\] ]*):$", text) |
666 returnType = re.search(r"->[ ]*([a-zA-Z0-9_,()\[\] ]*):$", text) |
697 if returnType: |
667 if returnType: |
698 self.returnTypeAnnotated = returnType.group(1) |
668 self.returnTypeAnnotated = returnType.group(1) |
699 textEnd = text.rfind(returnType.group(0)) |
669 textEnd = text.rfind(returnType.group(0)) |
700 else: |
670 else: |
701 self.returnTypeAnnotated = None |
671 self.returnTypeAnnotated = None |
702 textEnd = len(text) |
672 textEnd = len(text) |
703 |
673 |
704 positionArgumentsStart = text.find("(") + 1 |
674 positionArgumentsStart = text.find("(") + 1 |
705 positionArgumentsEnd = text.rfind(")", positionArgumentsStart, |
675 positionArgumentsEnd = text.rfind(")", positionArgumentsStart, textEnd) |
706 textEnd) |
676 |
707 |
|
708 self.argumentsText = text[positionArgumentsStart:positionArgumentsEnd] |
677 self.argumentsText = text[positionArgumentsStart:positionArgumentsEnd] |
709 |
678 |
710 argumentsList = self.__splitArgumentsTextToList(self.argumentsText) |
679 argumentsList = self.__splitArgumentsTextToList(self.argumentsText) |
711 if argumentsList is not None: |
680 if argumentsList is not None: |
712 self.hasInfo = True |
681 self.hasInfo = True |
713 self.__splitArgumentToNameTypeValue( |
682 self.__splitArgumentToNameTypeValue(argumentsList, quote, quoteReplace) |
714 argumentsList, quote, quoteReplace) |
683 |
715 |
|
716 functionName = ( |
684 functionName = ( |
717 text[:positionArgumentsStart - 1] |
685 text[: positionArgumentsStart - 1] |
718 .replace("async def ", "") |
686 .replace("async def ", "") |
719 .replace("def ", "") |
687 .replace("def ", "") |
720 ) |
688 ) |
721 if functionName == "__init__": |
689 if functionName == "__init__": |
722 self.functionType = "constructor" |
690 self.functionType = "constructor" |