eric6/Plugins/CheckerPlugins/CodeStyleChecker/DocStyleChecker.py

changeset 6942
2602857055c5
parent 6891
93f82da09f22
child 7243
cfeea029d8e1
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2013 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a checker for documentation string conventions.
8 """
9
10 #
11 # The routines of the checker class are modeled after the ones found in
12 # pep257.py (version 0.2.4).
13 #
14
15 try:
16 # Python 2
17 from StringIO import StringIO # __IGNORE_EXCEPTION__
18 except ImportError:
19 # Python 3
20 from io import StringIO # __IGNORE_WARNING__
21 import tokenize
22 import ast
23 import sys
24
25 try:
26 ast.AsyncFunctionDef # __IGNORE_EXCEPTION__
27 except AttributeError:
28 ast.AsyncFunctionDef = ast.FunctionDef
29
30
31 class DocStyleContext(object):
32 """
33 Class implementing the source context.
34 """
35 def __init__(self, source, startLine, contextType):
36 """
37 Constructor
38
39 @param source source code of the context (list of string or string)
40 @param startLine line number the context starts in the source (integer)
41 @param contextType type of the context object (string)
42 """
43 if sys.version_info[0] == 2:
44 stringTypes = (str, unicode) # __IGNORE_WARNING__
45 else:
46 stringTypes = str
47 if isinstance(source, stringTypes):
48 self.__source = source.splitlines(True)
49 else:
50 self.__source = source[:]
51 self.__start = startLine
52 self.__indent = ""
53 self.__type = contextType
54 self.__special = ""
55
56 # ensure first line is left justified
57 if self.__source:
58 self.__indent = self.__source[0].replace(
59 self.__source[0].lstrip(), "")
60 self.__source[0] = self.__source[0].lstrip()
61
62 def source(self):
63 """
64 Public method to get the source.
65
66 @return source (list of string)
67 """
68 return self.__source
69
70 def ssource(self):
71 """
72 Public method to get the joined source lines.
73
74 @return source (string)
75 """
76 return "".join(self.__source)
77
78 def start(self):
79 """
80 Public method to get the start line number.
81
82 @return start line number (integer)
83 """
84 return self.__start
85
86 def end(self):
87 """
88 Public method to get the end line number.
89
90 @return end line number (integer)
91 """
92 return self.__start + len(self.__source) - 1
93
94 def indent(self):
95 """
96 Public method to get the indentation of the first line.
97
98 @return indentation string (string)
99 """
100 return self.__indent
101
102 def contextType(self):
103 """
104 Public method to get the context type.
105
106 @return context type (string)
107 """
108 return self.__type
109
110 def setSpecial(self, special):
111 """
112 Public method to set a special attribute for the context.
113
114 @param special attribute string
115 @type str
116 """
117 self.__special = special
118
119 def special(self):
120 """
121 Public method to get the special context attribute string.
122
123 @return attribute string
124 @rtype str
125 """
126 return self.__special
127
128
129 class DocStyleChecker(object):
130 """
131 Class implementing a checker for documentation string conventions.
132 """
133 Codes = [
134 "D101", "D102", "D103", "D104", "D105",
135 "D111", "D112", "D113",
136 "D121", "D122",
137 "D130", "D131", "D132", "D133", "D134",
138 "D141", "D142", "D143", "D144", "D145",
139
140 "D203", "D205", "D206",
141 "D221", "D222",
142 "D231", "D232", "D234", "D235", "D236", "D237", "D238", "D239",
143 "D242", "D243", "D244", "D245", "D246", "D247",
144 "D250", "D251", "D252", "D253",
145 "D260", "D261", "D262", "D263",
146
147 "D901",
148 ]
149
150 def __init__(self, source, filename, select, ignore, expected, repeat,
151 maxLineLength=79, docType="pep257"):
152 """
153 Constructor
154
155 @param source source code to be checked (list of string)
156 @param filename name of the source file (string)
157 @param select list of selected codes (list of string)
158 @param ignore list of codes to be ignored (list of string)
159 @param expected list of expected codes (list of string)
160 @param repeat flag indicating to report each occurrence of a code
161 (boolean)
162 @keyparam maxLineLength allowed line length (integer)
163 @keyparam docType type of the documentation strings
164 (string, one of 'eric' or 'pep257')
165 """
166 assert docType in ("eric", "pep257")
167
168 self.__select = tuple(select)
169 self.__ignore = ('',) if select else tuple(ignore)
170 self.__expected = expected[:]
171 self.__repeat = repeat
172 self.__maxLineLength = maxLineLength
173 self.__docType = docType
174 self.__filename = filename
175 self.__source = source[:]
176
177 # statistics counters
178 self.counters = {}
179
180 # collection of detected errors
181 self.errors = []
182
183 self.__lineNumber = 0
184
185 # caches
186 self.__functionsCache = None
187 self.__classesCache = None
188 self.__methodsCache = None
189
190 self.__keywords = [
191 'moduleDocstring', 'functionDocstring',
192 'classDocstring', 'methodDocstring',
193 'defDocstring', 'docstring'
194 ]
195 if self.__docType == "pep257":
196 checkersWithCodes = {
197 "moduleDocstring": [
198 (self.__checkModulesDocstrings, ("D101",)),
199 ],
200 "functionDocstring": [
201 ],
202 "classDocstring": [
203 (self.__checkClassDocstring, ("D104", "D105")),
204 (self.__checkBlankBeforeAndAfterClass, ("D142", "D143")),
205 ],
206 "methodDocstring": [
207 ],
208 "defDocstring": [
209 (self.__checkFunctionDocstring, ("D102", "D103")),
210 (self.__checkImperativeMood, ("D132",)),
211 (self.__checkNoSignature, ("D133",)),
212 (self.__checkReturnType, ("D134",)),
213 (self.__checkNoBlankLineBefore, ("D141",)),
214 ],
215 "docstring": [
216 (self.__checkTripleDoubleQuotes, ("D111",)),
217 (self.__checkBackslashes, ("D112",)),
218 (self.__checkUnicode, ("D113",)),
219 (self.__checkOneLiner, ("D121",)),
220 (self.__checkIndent, ("D122",)),
221 (self.__checkSummary, ("D130",)),
222 (self.__checkEndsWithPeriod, ("D131",)),
223 (self.__checkBlankAfterSummary, ("D144",)),
224 (self.__checkBlankAfterLastParagraph, ("D145",)),
225 ],
226 }
227 elif self.__docType == "eric":
228 checkersWithCodes = {
229 "moduleDocstring": [
230 (self.__checkModulesDocstrings, ("D101",)),
231 ],
232 "functionDocstring": [
233 ],
234 "classDocstring": [
235 (self.__checkClassDocstring, ("D104", "D205", "D206")),
236 (self.__checkEricNoBlankBeforeAndAfterClassOrFunction,
237 ("D242", "D243")),
238 (self.__checkEricSignal, ("D260", "D261", "D262", "D263")),
239 ],
240 "methodDocstring": [
241 (self.__checkEricSummary, ("D232")),
242 ],
243 "defDocstring": [
244 (self.__checkFunctionDocstring, ("D102", "D203")),
245 (self.__checkImperativeMood, ("D132",)),
246 (self.__checkNoSignature, ("D133",)),
247 (self.__checkEricReturn, ("D234", "D235")),
248 (self.__checkEricFunctionArguments,
249 ("D236", "D237", "D238", "D239")),
250 (self.__checkEricNoBlankBeforeAndAfterClassOrFunction,
251 ("D244", "D245")),
252 (self.__checkEricException,
253 ("D250", "D251", "D252", "D253")),
254 ],
255 "docstring": [
256 (self.__checkTripleDoubleQuotes, ("D111",)),
257 (self.__checkBackslashes, ("D112",)),
258 (self.__checkUnicode, ("D113",)),
259 (self.__checkIndent, ("D122",)),
260 (self.__checkSummary, ("D130",)),
261 (self.__checkEricEndsWithPeriod, ("D231",)),
262 (self.__checkEricBlankAfterSummary, ("D246",)),
263 (self.__checkEricNBlankAfterLastParagraph, ("D247",)),
264 (self.__checkEricQuotesOnSeparateLines, ("D222", "D223"))
265 ],
266 }
267
268 self.__checkers = {}
269 for key, checkers in checkersWithCodes.items():
270 for checker, codes in checkers:
271 if any(not (code and self.__ignoreCode(code))
272 for code in codes):
273 if key not in self.__checkers:
274 self.__checkers[key] = []
275 self.__checkers[key].append(checker)
276
277 def __ignoreCode(self, code):
278 """
279 Private method to check if the error code should be ignored.
280
281 @param code message code to check for (string)
282 @return flag indicating to ignore the given code (boolean)
283 """
284 return (code.startswith(self.__ignore) and
285 not code.startswith(self.__select))
286
287 def __error(self, lineNumber, offset, code, *args):
288 """
289 Private method to record an issue.
290
291 @param lineNumber line number of the issue (integer)
292 @param offset position within line of the issue (integer)
293 @param code message code (string)
294 @param args arguments for the message (list)
295 """
296 if self.__ignoreCode(code):
297 return
298
299 if code in self.counters:
300 self.counters[code] += 1
301 else:
302 self.counters[code] = 1
303
304 # Don't care about expected codes
305 if code in self.__expected:
306 return
307
308 if code and (self.counters[code] == 1 or self.__repeat):
309 # record the issue with one based line number
310 self.errors.append(
311 (self.__filename, lineNumber + 1, offset, (code, args)))
312
313 def __reportInvalidSyntax(self):
314 """
315 Private method to report a syntax error.
316 """
317 exc_type, exc = sys.exc_info()[:2]
318 if len(exc.args) > 1:
319 offset = exc.args[1]
320 if len(offset) > 2:
321 offset = offset[1:3]
322 else:
323 offset = (1, 0)
324 self.__error(offset[0] - 1, offset[1] or 0,
325 'D901', exc_type.__name__, exc.args[0])
326
327 def __resetReadline(self):
328 """
329 Private method to reset the internal readline function.
330 """
331 self.__lineNumber = 0
332
333 def __readline(self):
334 """
335 Private method to get the next line from the source.
336
337 @return next line of source (string)
338 """
339 self.__lineNumber += 1
340 if self.__lineNumber > len(self.__source):
341 return ''
342 return self.__source[self.__lineNumber - 1]
343
344 def run(self):
345 """
346 Public method to check the given source for violations of doc string
347 conventions.
348 """
349 if not self.__filename:
350 # don't do anything, if essential data is missing
351 return
352
353 if not self.__checkers:
354 # don't do anything, if no codes were selected
355 return
356
357 source = "".join(self.__source)
358 # Check type for py2: if not str it's unicode
359 if sys.version_info[0] == 2:
360 try:
361 source = source.encode('utf-8')
362 except UnicodeError:
363 pass
364 try:
365 compile(source, self.__filename, 'exec', ast.PyCF_ONLY_AST)
366 except (SyntaxError, TypeError):
367 self.__reportInvalidSyntax()
368 return
369
370 for keyword in self.__keywords:
371 if keyword in self.__checkers:
372 for check in self.__checkers[keyword]:
373 for context in self.__parseContexts(keyword):
374 docstring = self.__parseDocstring(context, keyword)
375 check(docstring, context)
376
377 def __getSummaryLine(self, docstringContext):
378 """
379 Private method to extract the summary line.
380
381 @param docstringContext docstring context (DocStyleContext)
382 @return summary line (string) and the line it was found on (integer)
383 """
384 lines = docstringContext.source()
385
386 line = (lines[0]
387 .replace('r"""', "", 1)
388 .replace('u"""', "", 1)
389 .replace('"""', "")
390 .replace("r'''", "", 1)
391 .replace("u'''", "", 1)
392 .replace("'''", "")
393 .strip())
394
395 if len(lines) == 1 or len(line) > 0:
396 return line, 0
397 return lines[1].strip().replace('"""', "").replace("'''", ""), 1
398
399 def __getSummaryLines(self, docstringContext):
400 """
401 Private method to extract the summary lines.
402
403 @param docstringContext docstring context (DocStyleContext)
404 @return summary lines (list of string) and the line it was found on
405 (integer)
406 """
407 summaries = []
408 lines = docstringContext.source()
409
410 line0 = (lines[0]
411 .replace('r"""', "", 1)
412 .replace('u"""', "", 1)
413 .replace('"""', "")
414 .replace("r'''", "", 1)
415 .replace("u'''", "", 1)
416 .replace("'''", "")
417 .strip())
418 if len(lines) > 1:
419 line1 = lines[1].strip().replace('"""', "").replace("'''", "")
420 else:
421 line1 = ""
422 if len(lines) > 2:
423 line2 = lines[2].strip().replace('"""', "").replace("'''", "")
424 else:
425 line2 = ""
426 if line0:
427 lineno = 0
428 summaries.append(line0)
429 if not line0.endswith(".") and line1:
430 # two line summary
431 summaries.append(line1)
432 elif line1:
433 lineno = 1
434 summaries.append(line1)
435 if not line1.endswith(".") and line2:
436 # two line summary
437 summaries.append(line2)
438 else:
439 lineno = 2
440 summaries.append(line2)
441 return summaries, lineno
442
443 if sys.version_info[0] < 3:
444 def __getArgNames(self, node):
445 """
446 Private method to get the argument names of a function node.
447
448 @param node AST node to extract arguments names from
449 @return tuple of two list of argument names, one for arguments
450 and one for keyword arguments (tuple of list of string)
451 """
452 def unpackArgs(args):
453 """
454 Local helper function to unpack function argument names.
455
456 @param args list of AST node arguments
457 @return list of argument names (list of string)
458 """
459 ret = []
460 for arg in args:
461 if isinstance(arg, ast.Tuple):
462 ret.extend(unpackArgs(arg.elts))
463 else:
464 ret.append(arg.id)
465 return ret
466
467 arguments = unpackArgs(node.args.args)
468 if node.args.vararg is not None:
469 arguments.append(node.args.vararg)
470 kwarguments = []
471 if node.args.kwarg is not None:
472 kwarguments.append(node.args.kwarg)
473 return arguments, kwarguments
474 else:
475 def __getArgNames(self, node): # __IGNORE_WARNING__
476 """
477 Private method to get the argument names of a function node.
478
479 @param node AST node to extract arguments names from
480 @return tuple of two list of argument names, one for arguments
481 and one for keyword arguments (tuple of list of string)
482 """
483 arguments = []
484 arguments.extend([arg.arg for arg in node.args.args])
485 if node.args.vararg is not None:
486 if sys.version_info[1] < 4:
487 arguments.append(node.args.vararg)
488 else:
489 arguments.append(node.args.vararg.arg)
490
491 kwarguments = []
492 kwarguments.extend([arg.arg for arg in node.args.kwonlyargs])
493 if node.args.kwarg is not None:
494 if sys.version_info[1] < 4:
495 kwarguments.append(node.args.kwarg)
496 else:
497 kwarguments.append(node.args.kwarg.arg)
498 return arguments, kwarguments
499
500 ##################################################################
501 ## Parsing functionality below
502 ##################################################################
503
504 def __parseModuleDocstring(self, source):
505 """
506 Private method to extract a docstring given a module source.
507
508 @param source source to parse (list of string)
509 @return context of extracted docstring (DocStyleContext)
510 """
511 for kind, value, (line, _char), _, _ in tokenize.generate_tokens(
512 StringIO("".join(source)).readline):
513 if kind in [tokenize.COMMENT, tokenize.NEWLINE, tokenize.NL]:
514 continue
515 elif kind == tokenize.STRING: # first STRING should be docstring
516 return DocStyleContext(value, line - 1, "docstring")
517 else:
518 return None
519
520 return None
521
522 def __parseDocstring(self, context, what=''):
523 """
524 Private method to extract a docstring given `def` or `class` source.
525
526 @param context context data to get the docstring from (DocStyleContext)
527 @param what string denoting what is being parsed (string)
528 @return context of extracted docstring (DocStyleContext)
529 """
530 moduleDocstring = self.__parseModuleDocstring(context.source())
531 if what.startswith('module') or context.contextType() == "module":
532 return moduleDocstring
533 if moduleDocstring:
534 return moduleDocstring
535
536 tokenGenerator = tokenize.generate_tokens(
537 StringIO(context.ssource()).readline)
538 try:
539 kind = None
540 while kind != tokenize.INDENT:
541 kind, _, _, _, _ = next(tokenGenerator)
542 kind, value, (line, char), _, _ = next(tokenGenerator)
543 if kind == tokenize.STRING: # STRING after INDENT is a docstring
544 return DocStyleContext(
545 value, context.start() + line - 1, "docstring")
546 except StopIteration:
547 pass
548
549 return None
550
551 def __parseTopLevel(self, keyword):
552 """
553 Private method to extract top-level functions or classes.
554
555 @param keyword keyword signaling what to extract (string)
556 @return extracted function or class contexts (list of DocStyleContext)
557 """
558 self.__resetReadline()
559 tokenGenerator = tokenize.generate_tokens(self.__readline)
560 kind, value, char = None, None, None
561 contexts = []
562 try:
563 while True:
564 start, end = None, None
565 while not (kind == tokenize.NAME and
566 value == keyword and
567 char == 0):
568 kind, value, (line, char), _, _ = next(tokenGenerator)
569 start = line - 1, char
570 while not (kind == tokenize.DEDENT and
571 value == '' and
572 char == 0):
573 kind, value, (line, char), _, _ = next(tokenGenerator)
574 end = line - 1, char
575 contexts.append(DocStyleContext(
576 self.__source[start[0]:end[0]], start[0], keyword))
577 except StopIteration:
578 return contexts
579
580 def __parseFunctions(self):
581 """
582 Private method to extract top-level functions.
583
584 @return extracted function contexts (list of DocStyleContext)
585 """
586 if not self.__functionsCache:
587 self.__functionsCache = self.__parseTopLevel('def')
588 return self.__functionsCache
589
590 def __parseClasses(self):
591 """
592 Private method to extract top-level classes.
593
594 @return extracted class contexts (list of DocStyleContext)
595 """
596 if not self.__classesCache:
597 self.__classesCache = self.__parseTopLevel('class')
598 return self.__classesCache
599
600 def __skipIndentedBlock(self, tokenGenerator):
601 """
602 Private method to skip over an indented block of source code.
603
604 @param tokenGenerator token generator
605 @return last token of the indented block
606 """
607 kind, value, start, end, raw = next(tokenGenerator)
608 while kind != tokenize.INDENT:
609 kind, value, start, end, raw = next(tokenGenerator)
610 indent = 1
611 for kind, value, start, end, raw in tokenGenerator:
612 if kind == tokenize.INDENT:
613 indent += 1
614 elif kind == tokenize.DEDENT:
615 indent -= 1
616 if indent == 0:
617 return kind, value, start, end, raw
618
619 return None
620
621 def __parseMethods(self):
622 """
623 Private method to extract methods of all classes.
624
625 @return extracted method contexts (list of DocStyleContext)
626 """
627 if not self.__methodsCache:
628 contexts = []
629 for classContext in self.__parseClasses():
630 tokenGenerator = tokenize.generate_tokens(
631 StringIO(classContext.ssource()).readline)
632 kind, value, char = None, None, None
633 try:
634 while True:
635 start, end = None, None
636 while not (kind == tokenize.NAME and value == 'def'):
637 kind, value, (line, char), _, _ = \
638 next(tokenGenerator)
639 start = line - 1, char
640 kind, value, (line, char), _, _ = \
641 self.__skipIndentedBlock(tokenGenerator)
642 end = line - 1, char
643 startLine = classContext.start() + start[0]
644 endLine = classContext.start() + end[0]
645 context = DocStyleContext(
646 self.__source[startLine:endLine],
647 startLine, "def")
648 if startLine > 0:
649 if self.__source[startLine - 1].strip() == \
650 "@staticmethod":
651 context.setSpecial("staticmethod")
652 elif self.__source[startLine - 1].strip() == \
653 "@classmethod":
654 context.setSpecial("classmethod")
655 contexts.append(context)
656 except StopIteration:
657 pass
658 self.__methodsCache = contexts
659
660 return self.__methodsCache
661
662 def __parseContexts(self, kind):
663 """
664 Private method to extract a context from the source.
665
666 @param kind kind of context to extract (string)
667 @return requested contexts (list of DocStyleContext)
668 """
669 if kind == 'moduleDocstring':
670 return [DocStyleContext(self.__source, 0, "module")]
671 if kind == 'functionDocstring':
672 return self.__parseFunctions()
673 if kind == 'classDocstring':
674 return self.__parseClasses()
675 if kind == 'methodDocstring':
676 return self.__parseMethods()
677 if kind == 'defDocstring':
678 return self.__parseFunctions() + self.__parseMethods()
679 if kind == 'docstring':
680 return ([DocStyleContext(self.__source, 0, "module")] +
681 self.__parseFunctions() +
682 self.__parseClasses() +
683 self.__parseMethods())
684 return [] # fall back
685
686 ##################################################################
687 ## Checking functionality below (PEP-257)
688 ##################################################################
689
690 def __checkModulesDocstrings(self, docstringContext, context):
691 """
692 Private method to check, if the module has a docstring.
693
694 @param docstringContext docstring context (DocStyleContext)
695 @param context context of the docstring (DocStyleContext)
696 """
697 if docstringContext is None:
698 self.__error(context.start(), 0, "D101")
699 return
700
701 docstring = docstringContext.ssource()
702 if (not docstring or not docstring.strip() or
703 not docstring.strip('\'"')):
704 self.__error(context.start(), 0, "D101")
705
706 if self.__docType == "eric" and \
707 docstring.strip('\'"').strip() == "Module documentation goes here.":
708 self.__error(docstringContext.end(), 0, "D201")
709 return
710
711 def __checkFunctionDocstring(self, docstringContext, context):
712 """
713 Private method to check, that all public functions and methods
714 have a docstring.
715
716 @param docstringContext docstring context (DocStyleContext)
717 @param context context of the docstring (DocStyleContext)
718 """
719 functionName = context.source()[0].lstrip().split()[1].split("(")[0]
720 if functionName.startswith('_') and not functionName.endswith('__'):
721 if self.__docType == "eric":
722 code = "D203"
723 else:
724 code = "D103"
725 else:
726 code = "D102"
727
728 if docstringContext is None:
729 self.__error(context.start(), 0, code)
730 return
731
732 docstring = docstringContext.ssource()
733 if (not docstring or not docstring.strip() or
734 not docstring.strip('\'"')):
735 self.__error(context.start(), 0, code)
736
737 if self.__docType == "eric" and \
738 docstring.strip('\'"').strip() == \
739 "Function documentation goes here.":
740 self.__error(docstringContext.end(), 0, "D202")
741 return
742
743 def __checkClassDocstring(self, docstringContext, context):
744 """
745 Private method to check, that all public functions and methods
746 have a docstring.
747
748 @param docstringContext docstring context (DocStyleContext)
749 @param context context of the docstring (DocStyleContext)
750 """
751 className = context.source()[0].lstrip().split()[1].split("(")[0]
752 if className.startswith('_'):
753 if self.__docType == "eric":
754 code = "D205"
755 else:
756 code = "D105"
757 else:
758 code = "D104"
759
760 if docstringContext is None:
761 self.__error(context.start(), 0, code)
762 return
763
764 docstring = docstringContext.ssource()
765 if (not docstring or not docstring.strip() or
766 not docstring.strip('\'"')):
767 self.__error(context.start(), 0, code)
768 return
769
770 if self.__docType == "eric" and \
771 docstring.strip('\'"').strip() == "Class documentation goes here.":
772 self.__error(docstringContext.end(), 0, "D206")
773 return
774
775 def __checkTripleDoubleQuotes(self, docstringContext, context):
776 """
777 Private method to check, that all docstrings are surrounded
778 by triple double quotes.
779
780 @param docstringContext docstring context (DocStyleContext)
781 @param context context of the docstring (DocStyleContext)
782 """
783 if docstringContext is None:
784 return
785
786 docstring = docstringContext.ssource().strip()
787 if not docstring.startswith(('"""', 'r"""', 'u"""')):
788 self.__error(docstringContext.start(), 0, "D111")
789
790 def __checkBackslashes(self, docstringContext, context):
791 """
792 Private method to check, that all docstrings containing
793 backslashes are surrounded by raw triple double quotes.
794
795 @param docstringContext docstring context (DocStyleContext)
796 @param context context of the docstring (DocStyleContext)
797 """
798 if docstringContext is None:
799 return
800
801 docstring = docstringContext.ssource().strip()
802 if "\\" in docstring and not docstring.startswith('r"""'):
803 self.__error(docstringContext.start(), 0, "D112")
804
805 def __checkUnicode(self, docstringContext, context):
806 """
807 Private method to check, that all docstrings containing unicode
808 characters are surrounded by unicode triple double quotes.
809
810 @param docstringContext docstring context (DocStyleContext)
811 @param context context of the docstring (DocStyleContext)
812 """
813 if docstringContext is None:
814 return
815
816 docstring = docstringContext.ssource().strip()
817 if not docstring.startswith('u"""') and \
818 any(ord(char) > 127 for char in docstring):
819 self.__error(docstringContext.start(), 0, "D113")
820
821 def __checkOneLiner(self, docstringContext, context):
822 """
823 Private method to check, that one-liner docstrings fit on
824 one line with quotes.
825
826 @param docstringContext docstring context (DocStyleContext)
827 @param context context of the docstring (DocStyleContext)
828 """
829 if docstringContext is None:
830 return
831
832 lines = docstringContext.source()
833 if len(lines) > 1:
834 nonEmptyLines = [l for l in lines if l.strip().strip('\'"')]
835 if len(nonEmptyLines) == 1:
836 modLen = len(context.indent() + '"""' +
837 nonEmptyLines[0].strip() + '"""')
838 if context.contextType() != "module":
839 modLen += 4
840 if not nonEmptyLines[0].strip().endswith("."):
841 # account for a trailing dot
842 modLen += 1
843 if modLen <= self.__maxLineLength:
844 self.__error(docstringContext.start(), 0, "D121")
845
846 def __checkIndent(self, docstringContext, context):
847 """
848 Private method to check, that docstrings are properly indented.
849
850 @param docstringContext docstring context (DocStyleContext)
851 @param context context of the docstring (DocStyleContext)
852 """
853 if docstringContext is None:
854 return
855
856 lines = docstringContext.source()
857 if len(lines) == 1:
858 return
859
860 nonEmptyLines = [l.rstrip() for l in lines[1:] if l.strip()]
861 if not nonEmptyLines:
862 return
863
864 indent = min(len(l) - len(l.strip()) for l in nonEmptyLines)
865 if context.contextType() == "module":
866 expectedIndent = 0
867 else:
868 expectedIndent = len(context.indent()) + 4
869 if indent != expectedIndent:
870 self.__error(docstringContext.start(), 0, "D122")
871
872 def __checkSummary(self, docstringContext, context):
873 """
874 Private method to check, that docstring summaries contain some text.
875
876 @param docstringContext docstring context (DocStyleContext)
877 @param context context of the docstring (DocStyleContext)
878 """
879 if docstringContext is None:
880 return
881
882 summary, lineNumber = self.__getSummaryLine(docstringContext)
883 if summary == "":
884 self.__error(docstringContext.start() + lineNumber, 0, "D130")
885
886 def __checkEndsWithPeriod(self, docstringContext, context):
887 """
888 Private method to check, that docstring summaries end with a period.
889
890 @param docstringContext docstring context (DocStyleContext)
891 @param context context of the docstring (DocStyleContext)
892 """
893 if docstringContext is None:
894 return
895
896 summary, lineNumber = self.__getSummaryLine(docstringContext)
897 if not summary.endswith("."):
898 self.__error(docstringContext.start() + lineNumber, 0, "D131")
899
900 def __checkImperativeMood(self, docstringContext, context):
901 """
902 Private method to check, that docstring summaries are in
903 imperative mood.
904
905 @param docstringContext docstring context (DocStyleContext)
906 @param context context of the docstring (DocStyleContext)
907 """
908 if docstringContext is None:
909 return
910
911 summary, lineNumber = self.__getSummaryLine(docstringContext)
912 if summary:
913 firstWord = summary.strip().split()[0]
914 if firstWord.endswith("s") and not firstWord.endswith("ss"):
915 self.__error(docstringContext.start() + lineNumber, 0, "D132")
916
917 def __checkNoSignature(self, docstringContext, context):
918 """
919 Private method to check, that docstring summaries don't repeat
920 the function's signature.
921
922 @param docstringContext docstring context (DocStyleContext)
923 @param context context of the docstring (DocStyleContext)
924 """
925 if docstringContext is None:
926 return
927
928 functionName = context.source()[0].lstrip().split()[1].split("(")[0]
929 summary, lineNumber = self.__getSummaryLine(docstringContext)
930 if functionName + "(" in summary.replace(" ", "") and \
931 not functionName + "()" in summary.replace(" ", ""):
932 # report only, if it is not an abbreviated form (i.e. function() )
933 self.__error(docstringContext.start() + lineNumber, 0, "D133")
934
935 def __checkReturnType(self, docstringContext, context):
936 """
937 Private method to check, that docstrings mention the return value type.
938
939 @param docstringContext docstring context (DocStyleContext)
940 @param context context of the docstring (DocStyleContext)
941 """
942 if docstringContext is None:
943 return
944
945 if "return" not in docstringContext.ssource().lower():
946 tokens = list(
947 tokenize.generate_tokens(StringIO(context.ssource()).readline))
948 return_ = [tokens[i + 1][0] for i, token in enumerate(tokens)
949 if token[1] == "return"]
950 if (set(return_) -
951 {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} !=
952 set()):
953 self.__error(docstringContext.end(), 0, "D134")
954
955 def __checkNoBlankLineBefore(self, docstringContext, context):
956 """
957 Private method to check, that function/method docstrings are not
958 preceded by a blank line.
959
960 @param docstringContext docstring context (DocStyleContext)
961 @param context context of the docstring (DocStyleContext)
962 """
963 if docstringContext is None:
964 return
965
966 contextLines = context.source()
967 cti = 0
968 while cti < len(contextLines) and \
969 not contextLines[cti].strip().startswith(
970 ('"""', 'r"""', 'u"""', "'''", "r'''", "u'''")):
971 cti += 1
972 if cti == len(contextLines):
973 return
974
975 if not contextLines[cti - 1].strip():
976 self.__error(docstringContext.start(), 0, "D141")
977
978 def __checkBlankBeforeAndAfterClass(self, docstringContext, context):
979 """
980 Private method to check, that class docstrings have one
981 blank line around them.
982
983 @param docstringContext docstring context (DocStyleContext)
984 @param context context of the docstring (DocStyleContext)
985 """
986 if docstringContext is None:
987 return
988
989 contextLines = context.source()
990 cti = 0
991 while cti < len(contextLines) and \
992 not contextLines[cti].strip().startswith(
993 ('"""', 'r"""', 'u"""', "'''", "r'''", "u'''")):
994 cti += 1
995 if cti == len(contextLines):
996 return
997
998 start = cti
999 if contextLines[cti].strip() in (
1000 '"""', 'r"""', 'u"""', "'''", "r'''", "u'''"):
1001 # it is a multi line docstring
1002 cti += 1
1003
1004 while cti < len(contextLines) and \
1005 not contextLines[cti].strip().endswith(('"""', "'''")):
1006 cti += 1
1007 end = cti
1008 if cti >= len(contextLines) - 1:
1009 return
1010
1011 if contextLines[start - 1].strip():
1012 self.__error(docstringContext.start(), 0, "D142")
1013 if contextLines[end + 1].strip():
1014 self.__error(docstringContext.end(), 0, "D143")
1015
1016 def __checkBlankAfterSummary(self, docstringContext, context):
1017 """
1018 Private method to check, that docstring summaries are followed
1019 by a blank line.
1020
1021 @param docstringContext docstring context (DocStyleContext)
1022 @param context context of the docstring (DocStyleContext)
1023 """
1024 if docstringContext is None:
1025 return
1026
1027 docstrings = docstringContext.source()
1028 if len(docstrings) <= 3:
1029 # correct/invalid one-liner
1030 return
1031
1032 summary, lineNumber = self.__getSummaryLine(docstringContext)
1033 if len(docstrings) > 2:
1034 if docstrings[lineNumber + 1].strip():
1035 self.__error(docstringContext.start() + lineNumber, 0, "D144")
1036
1037 def __checkBlankAfterLastParagraph(self, docstringContext, context):
1038 """
1039 Private method to check, that the last paragraph of docstrings is
1040 followed by a blank line.
1041
1042 @param docstringContext docstring context (DocStyleContext)
1043 @param context context of the docstring (DocStyleContext)
1044 """
1045 if docstringContext is None:
1046 return
1047
1048 docstrings = docstringContext.source()
1049 if len(docstrings) <= 3:
1050 # correct/invalid one-liner
1051 return
1052
1053 if docstrings[-2].strip():
1054 self.__error(docstringContext.end(), 0, "D145")
1055
1056 ##################################################################
1057 ## Checking functionality below (eric specific ones)
1058 ##################################################################
1059
1060 def __checkEricQuotesOnSeparateLines(self, docstringContext, context):
1061 """
1062 Private method to check, that leading and trailing quotes are on
1063 a line by themselves.
1064
1065 @param docstringContext docstring context (DocStyleContext)
1066 @param context context of the docstring (DocStyleContext)
1067 """
1068 if docstringContext is None:
1069 return
1070
1071 lines = docstringContext.source()
1072 if lines[0].strip().strip('ru"\''):
1073 self.__error(docstringContext.start(), 0, "D221")
1074 if lines[-1].strip().strip('"\''):
1075 self.__error(docstringContext.end(), 0, "D222")
1076
1077 def __checkEricEndsWithPeriod(self, docstringContext, context):
1078 """
1079 Private method to check, that docstring summaries end with a period.
1080
1081 @param docstringContext docstring context (DocStyleContext)
1082 @param context context of the docstring (DocStyleContext)
1083 """
1084 if docstringContext is None:
1085 return
1086
1087 summaryLines, lineNumber = self.__getSummaryLines(docstringContext)
1088 if summaryLines:
1089 if summaryLines[-1].lstrip().startswith("@"):
1090 summaryLines.pop(-1)
1091 summary = " ".join([s.strip() for s in summaryLines if s])
1092 if summary and not summary.endswith(".") and \
1093 not summary.split(None, 1)[0].lower() == "constructor":
1094 self.__error(
1095 docstringContext.start() + lineNumber +
1096 len(summaryLines) - 1,
1097 0, "D231")
1098
1099 def __checkEricReturn(self, docstringContext, context):
1100 """
1101 Private method to check, that docstrings contain an &#64;return line
1102 if they return anything and don't otherwise.
1103
1104 @param docstringContext docstring context (DocStyleContext)
1105 @param context context of the docstring (DocStyleContext)
1106 """
1107 if docstringContext is None:
1108 return
1109
1110 tokens = list(
1111 tokenize.generate_tokens(StringIO(context.ssource()).readline))
1112 return_ = [tokens[i + 1][0] for i, token in enumerate(tokens)
1113 if token[1] in ("return", "yield")]
1114 if "@return" not in docstringContext.ssource():
1115 if (set(return_) -
1116 {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} !=
1117 set()):
1118 self.__error(docstringContext.end(), 0, "D234")
1119 else:
1120 if (set(return_) -
1121 {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} ==
1122 set()):
1123 self.__error(docstringContext.end(), 0, "D235")
1124
1125 def __checkEricFunctionArguments(self, docstringContext, context):
1126 """
1127 Private method to check, that docstrings contain an &#64;param and/or
1128 &#64;keyparam line for each argument.
1129
1130 @param docstringContext docstring context (DocStyleContext)
1131 @param context context of the docstring (DocStyleContext)
1132 """
1133 if docstringContext is None:
1134 return
1135
1136 try:
1137 tree = ast.parse(context.ssource())
1138 except (SyntaxError, TypeError):
1139 return
1140 if (isinstance(tree, ast.Module) and len(tree.body) == 1 and
1141 isinstance(tree.body[0],
1142 (ast.FunctionDef, ast.AsyncFunctionDef))):
1143 functionDef = tree.body[0]
1144 argNames, kwNames = self.__getArgNames(functionDef)
1145 if "self" in argNames:
1146 argNames.remove("self")
1147 if "cls" in argNames:
1148 argNames.remove("cls")
1149
1150 docstring = docstringContext.ssource()
1151 if (docstring.count("@param") + docstring.count("@keyparam") <
1152 len(argNames + kwNames)):
1153 self.__error(docstringContext.end(), 0, "D236")
1154 elif (docstring.count("@param") + docstring.count("@keyparam") >
1155 len(argNames + kwNames)):
1156 self.__error(docstringContext.end(), 0, "D237")
1157 else:
1158 # extract @param and @keyparam from docstring
1159 args = []
1160 kwargs = []
1161 for line in docstringContext.source():
1162 if line.strip().startswith(("@param", "@keyparam")):
1163 paramParts = line.strip().split(None, 2)
1164 if len(paramParts) >= 2:
1165 at, name = paramParts[:2]
1166 if at == "@keyparam":
1167 kwargs.append(name.lstrip("*"))
1168 args.append(name.lstrip("*"))
1169
1170 # do the checks
1171 for name in kwNames:
1172 if name not in kwargs:
1173 self.__error(docstringContext.end(), 0, "D238")
1174 return
1175 if argNames + kwNames != args:
1176 self.__error(docstringContext.end(), 0, "D239")
1177
1178 def __checkEricException(self, docstringContext, context):
1179 """
1180 Private method to check, that docstrings contain an &#64;exception line
1181 if they raise an exception and don't otherwise.
1182
1183 Note: This method also checks the raised and documented exceptions for
1184 completeness (i.e. raised exceptions that are not documented or
1185 documented exceptions that are not raised)
1186
1187 @param docstringContext docstring context (DocStyleContext)
1188 @param context context of the docstring (DocStyleContext)
1189 """
1190 if docstringContext is None:
1191 return
1192
1193 tokens = list(
1194 tokenize.generate_tokens(StringIO(context.ssource()).readline))
1195 exceptions = set()
1196 raisedExceptions = set()
1197 tokensLen = len(tokens)
1198 for i, token in enumerate(tokens):
1199 if token[1] == "raise":
1200 exceptions.add(tokens[i + 1][0])
1201 if tokens[i + 1][0] == tokenize.NAME:
1202 if tokensLen > (i + 2) and \
1203 tokens[i + 2][1] == ".":
1204 raisedExceptions.add("{0}.{1}".format(
1205 tokens[i + 1][1], tokens[i + 3][1]))
1206 else:
1207 raisedExceptions.add(tokens[i + 1][1])
1208
1209 if "@exception" not in docstringContext.ssource() and \
1210 "@throws" not in docstringContext.ssource() and \
1211 "@raise" not in docstringContext.ssource():
1212 if (exceptions -
1213 {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} !=
1214 set()):
1215 self.__error(docstringContext.end(), 0, "D250")
1216 else:
1217 if (exceptions -
1218 {tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE} ==
1219 set()):
1220 self.__error(docstringContext.end(), 0, "D251")
1221 else:
1222 # step 1: extract documented exceptions
1223 documentedExceptions = set()
1224 for line in docstringContext.source():
1225 line = line.strip()
1226 if line.startswith(("@exception", "@throws", "@raise")):
1227 exceptionTokens = line.split(None, 2)
1228 if len(exceptionTokens) >= 2:
1229 documentedExceptions.add(exceptionTokens[1])
1230
1231 # step 2: report undocumented exceptions
1232 for exception in raisedExceptions:
1233 if exception not in documentedExceptions:
1234 self.__error(docstringContext.end(), 0, "D252",
1235 exception)
1236
1237 # step 3: report undefined signals
1238 for exception in documentedExceptions:
1239 if exception not in raisedExceptions:
1240 self.__error(docstringContext.end(), 0, "D253",
1241 exception)
1242
1243 def __checkEricSignal(self, docstringContext, context):
1244 """
1245 Private method to check, that docstrings contain an &#64;signal line
1246 if they define signals and don't otherwise.
1247
1248 Note: This method also checks the defined and documented signals for
1249 completeness (i.e. defined signals that are not documented or
1250 documented signals that are not defined)
1251
1252 @param docstringContext docstring context (DocStyleContext)
1253 @param context context of the docstring (DocStyleContext)
1254 """
1255 if docstringContext is None:
1256 return
1257
1258 tokens = list(
1259 tokenize.generate_tokens(StringIO(context.ssource()).readline))
1260 definedSignals = set()
1261 for i, token in enumerate(tokens):
1262 if token[1] in ("pyqtSignal", "Signal"):
1263 if tokens[i - 1][1] == "." and tokens[i - 2][1] == "QtCore":
1264 definedSignals.add(tokens[i - 4][1])
1265 elif tokens[i - 1][1] == "=":
1266 definedSignals.add(tokens[i - 2][1])
1267
1268 if "@signal" not in docstringContext.ssource() and definedSignals:
1269 self.__error(docstringContext.end(), 0, "D260")
1270 elif "@signal" in docstringContext.ssource():
1271 if not definedSignals:
1272 self.__error(docstringContext.end(), 0, "D261")
1273 else:
1274 # step 1: extract documented signals
1275 documentedSignals = set()
1276 for line in docstringContext.source():
1277 line = line.strip()
1278 if line.startswith("@signal"):
1279 signalTokens = line.split(None, 2)
1280 if len(signalTokens) >= 2:
1281 signal = signalTokens[1]
1282 if "(" in signal:
1283 signal = signal.split("(", 1)[0]
1284 documentedSignals.add(signal)
1285
1286 # step 2: report undocumented signals
1287 for signal in definedSignals:
1288 if signal not in documentedSignals:
1289 self.__error(docstringContext.end(), 0, "D262", signal)
1290
1291 # step 3: report undefined signals
1292 for signal in documentedSignals:
1293 if signal not in definedSignals:
1294 self.__error(docstringContext.end(), 0, "D263", signal)
1295
1296 def __checkEricBlankAfterSummary(self, docstringContext, context):
1297 """
1298 Private method to check, that docstring summaries are followed
1299 by a blank line.
1300
1301 @param docstringContext docstring context (DocStyleContext)
1302 @param context context of the docstring (DocStyleContext)
1303 """
1304 if docstringContext is None:
1305 return
1306
1307 docstrings = docstringContext.source()
1308 if len(docstrings) <= 3:
1309 # correct/invalid one-liner
1310 return
1311
1312 summaryLines, lineNumber = self.__getSummaryLines(docstringContext)
1313 if len(docstrings) - 2 > lineNumber + len(summaryLines) - 1:
1314 if docstrings[lineNumber + len(summaryLines)].strip():
1315 self.__error(docstringContext.start() + lineNumber, 0, "D246")
1316
1317 def __checkEricNoBlankBeforeAndAfterClassOrFunction(
1318 self, docstringContext, context):
1319 """
1320 Private method to check, that class and function/method docstrings
1321 have no blank line around them.
1322
1323 @param docstringContext docstring context (DocStyleContext)
1324 @param context context of the docstring (DocStyleContext)
1325 """
1326 if docstringContext is None:
1327 return
1328
1329 contextLines = context.source()
1330 isClassContext = contextLines[0].lstrip().startswith("class ")
1331 cti = 0
1332 while cti < len(contextLines) and \
1333 not contextLines[cti].strip().startswith(
1334 ('"""', 'r"""', 'u"""', "'''", "r'''", "u'''")):
1335 cti += 1
1336 if cti == len(contextLines):
1337 return
1338
1339 start = cti
1340 if contextLines[cti].strip() in (
1341 '"""', 'r"""', 'u"""', "'''", "r'''", "u'''"):
1342 # it is a multi line docstring
1343 cti += 1
1344
1345 while cti < len(contextLines) and \
1346 not contextLines[cti].strip().endswith(('"""', "'''")):
1347 cti += 1
1348 end = cti
1349 if cti >= len(contextLines) - 1:
1350 return
1351
1352 if isClassContext:
1353 if not contextLines[start - 1].strip():
1354 self.__error(docstringContext.start(), 0, "D242")
1355 if not contextLines[end + 1].strip():
1356 self.__error(docstringContext.end(), 0, "D243")
1357 else:
1358 if not contextLines[start - 1].strip():
1359 self.__error(docstringContext.start(), 0, "D244")
1360 if not contextLines[end + 1].strip():
1361 self.__error(docstringContext.end(), 0, "D245")
1362
1363 def __checkEricNBlankAfterLastParagraph(self, docstringContext, context):
1364 """
1365 Private method to check, that the last paragraph of docstrings is
1366 not followed by a blank line.
1367
1368 @param docstringContext docstring context (DocStyleContext)
1369 @param context context of the docstring (DocStyleContext)
1370 """
1371 if docstringContext is None:
1372 return
1373
1374 docstrings = docstringContext.source()
1375 if len(docstrings) <= 3:
1376 # correct/invalid one-liner
1377 return
1378
1379 if not docstrings[-2].strip():
1380 self.__error(docstringContext.end(), 0, "D247")
1381
1382 def __checkEricSummary(self, docstringContext, context):
1383 """
1384 Private method to check, that method docstring summaries start with
1385 specific words.
1386
1387 @param docstringContext docstring context (DocStyleContext)
1388 @param context context of the docstring (DocStyleContext)
1389 """
1390 if docstringContext is None:
1391 return
1392
1393 summary, lineNumber = self.__getSummaryLine(docstringContext)
1394 if summary:
1395 # check, if the first word is 'Constructor', 'Public',
1396 # 'Protected' or 'Private'
1397 functionName, arguments = context.source()[0].lstrip()\
1398 .split()[1].split("(", 1)
1399 firstWord = summary.strip().split(None, 1)[0].lower()
1400 if functionName == '__init__':
1401 if firstWord != 'constructor':
1402 self.__error(docstringContext.start() + lineNumber, 0,
1403 "D232", 'constructor')
1404 elif functionName.startswith('__') and \
1405 functionName.endswith('__'):
1406 if firstWord != 'special':
1407 self.__error(docstringContext.start() + lineNumber, 0,
1408 "D232", 'special')
1409 elif context.special() == "staticmethod":
1410 secondWord = summary.strip().split(None, 2)[1].lower()
1411 if firstWord != 'static' and secondWord != 'static':
1412 self.__error(docstringContext.start() + lineNumber, 0,
1413 "D232", 'static')
1414 elif secondWord == 'static':
1415 if functionName.startswith(('__', 'on_')):
1416 if firstWord != 'private':
1417 self.__error(docstringContext.start() + lineNumber,
1418 0, "D232", 'private static')
1419 elif functionName.startswith('_') or \
1420 functionName.endswith('Event'):
1421 if firstWord != 'protected':
1422 self.__error(docstringContext.start() + lineNumber,
1423 0, "D232", 'protected static')
1424 else:
1425 if firstWord != 'public':
1426 self.__error(docstringContext.start() + lineNumber,
1427 0, "D232", 'public static')
1428 elif arguments.startswith(('cls,', 'cls)')) or \
1429 context.special() == "classmethod":
1430 secondWord = summary.strip().split(None, 2)[1].lower()
1431 if firstWord != 'class' and secondWord != 'class':
1432 self.__error(docstringContext.start() + lineNumber, 0,
1433 "D232", 'class')
1434 elif secondWord == 'class':
1435 if functionName.startswith(('__', 'on_')):
1436 if firstWord != 'private':
1437 self.__error(docstringContext.start() + lineNumber,
1438 0, "D232", 'private class')
1439 elif functionName.startswith('_') or \
1440 functionName.endswith('Event'):
1441 if firstWord != 'protected':
1442 self.__error(docstringContext.start() + lineNumber,
1443 0, "D232", 'protected class')
1444 else:
1445 if firstWord != 'public':
1446 self.__error(docstringContext.start() + lineNumber,
1447 0, "D232", 'public class')
1448 elif functionName.startswith(('__', 'on_')):
1449 if firstWord != 'private':
1450 self.__error(docstringContext.start() + lineNumber, 0,
1451 "D232", 'private')
1452 elif functionName.startswith('_') or \
1453 functionName.endswith('Event'):
1454 if firstWord != 'protected':
1455 self.__error(docstringContext.start() + lineNumber, 0,
1456 "D232", 'protected')
1457 else:
1458 if firstWord != 'public':
1459 self.__error(docstringContext.start() + lineNumber, 0,
1460 "D232", 'public')
1461
1462 #
1463 # eflag: noqa = M702

eric ide

mercurial