Exempt overriding methods from docstring_linter (fix #151692) (#151906)

Pull Request resolved: https://github.com/pytorch/pytorch/pull/151906
Approved by: https://github.com/Skylion007
This commit is contained in:
Tom Ritchford 2025-04-29 15:39:41 +00:00 committed by PyTorch MergeBot
parent 9210a98b92
commit 2825a28bf1
13 changed files with 318 additions and 75 deletions

View File

@ -8,7 +8,7 @@ import token
from enum import Enum
from functools import cached_property, total_ordering
from pathlib import Path
from typing import Any, Callable, TYPE_CHECKING
from typing import Any, Callable, Optional, TYPE_CHECKING
from typing_extensions import Self
@ -34,10 +34,15 @@ MAX_LINES = {"class": 100, "def": 80}
MIN_DOCSTRING = 50 # docstrings shorter than this are too short
ERROR_FMT = "Every {type} with more than {length} lines needs a docstring"
DESCRIPTION = """
`docstring_linter` reports on long functions, methods or classes without docstrings
""".strip()
DESCRIPTION = """`docstring_linter` reports on long functions, methods or classes
without docstrings"""
METHOD_OVERRIDE_HINT = (
"If the method overrides a method on a parent class, adding the"
" `@typing_extensions.override` decorator will make this error"
" go away."
)
@total_ordering
@ -87,7 +92,7 @@ class Block:
is_method: bool = dc.field(default=False, repr=False)
# A block index to the parent of this block, or None for a top-level block.
parent: int | None = None
parent: Optional[int] = None
# A list of block indexes for the children
children: list[int] = dc.field(default_factory=list)
@ -114,9 +119,25 @@ class Block:
ending = "" if self.is_class else "()"
return f"{self.category.value} {self.full_name}{ending}"
@cached_property
def decorators(self) -> list[str]:
"""A list of decorators for this function or method.
Each decorator both the @ symbol and any arguments to the decorator
but no extra whitespace.
"""
return _get_decorators(self.tokens, self.begin)
@cached_property
def is_override(self) -> bool:
return not self.is_class and any(
d.rpartition(".")[2] == "override" for d in self.decorators
)
DATA_FIELDS = (
"category",
"children",
"decorators",
"display_name",
"docstring",
"full_name",
@ -152,6 +173,33 @@ class Block:
return o.index < self.index
_IGNORE = {token.COMMENT, token.DEDENT, token.INDENT, token.NL}
def _get_decorators(tokens: Sequence[TokenInfo], block_start: int) -> list[str]:
def decorators() -> Iterator[str]:
rev = reversed(range(block_start))
newlines = (i for i in rev if tokens[i].type == token.NEWLINE)
newlines = itertools.chain(newlines, [-1]) # To account for the first line
it = iter(newlines)
end = next(it, -1) # Like itertools.pairwise in Python 3.10
for begin in it:
for i in range(begin + 1, end):
t = tokens[i]
if t.type == token.OP and t.string == "@":
useful = (t for t in tokens[i:end] if t.type not in _IGNORE)
yield "".join(s.string.strip("\n") for s in useful)
break
elif t.type not in _IGNORE:
return # A statement means no more decorators
end = begin
out = list(decorators())
out.reverse()
return out
class DocstringFile(_linter.PythonFile):
def __getitem__(self, i: int | slice) -> TokenInfo | Sequence[TokenInfo]:
return self.tokens[i]
@ -337,9 +385,13 @@ class DocstringLinter(_linter.FileLinter[DocstringFile]):
def_name = "function" if b.category == "def" else "class"
msg = f"docstring found for {def_name} '{b.name}' ({b.line_count} lines)"
if len(b.docstring):
msg = msg + f" was too short ({len(b.docstring)} characters)"
s = "" if len(b.docstring) == 1 else "s"
needed = f"needed {self.args.min_docstring}"
msg = f"{msg} was too short ({len(b.docstring)} character{s}, {needed})"
else:
msg = "No " + msg
msg = f"No {msg}"
if b.is_method:
msg = f"{msg}. {METHOD_OVERRIDE_HINT}"
return _linter.LintResult(msg, *df.tokens[b.begin].start)
def _display(

View File

@ -14,7 +14,7 @@ tools/test/docstring_linter_testdata/python_code.py.txt:17: No docstring found f
18 | # A comment isn't a docstring
19 |
tools/test/docstring_linter_testdata/python_code.py.txt:24: docstring found for class 'LongWithShortDocstring' (6 lines) was too short (10 characters)
tools/test/docstring_linter_testdata/python_code.py.txt:24: docstring found for class 'LongWithShortDocstring' (6 lines) was too short (10 characters, needed 16)
22 |
23 |
24 | class LongWithShortDocstring:
@ -22,7 +22,7 @@ tools/test/docstring_linter_testdata/python_code.py.txt:24: docstring found for
25 | """TODO"""
26 |
tools/test/docstring_linter_testdata/python_code.py.txt:71: No docstring found for function 'needs_docs' (12 lines)
tools/test/docstring_linter_testdata/python_code.py.txt:71: No docstring found for function 'needs_docs' (12 lines). If the method overrides a method on a parent class, adding the `@typing_extensions.override` decorator will make this error go away.
69 | """This docstring, while short, is enough"""
70 |
71 | def needs_docs(self):

View File

@ -81,12 +81,21 @@ class ImpossibleCombo(
pass
@override # Won't work!
class NotDocstring:
def short1(self):
pass
"""This is not a docstring"""
@override
def long_with_override(self):
#
#
#
#
pass
def short2(self):
pass
@ -94,6 +103,7 @@ class NotDocstring:
pass
def long_with_omit(): # noqa: docstring_linter
#
#

View File

@ -15,7 +15,7 @@
"code": "DOCSTRING_LINTER",
"description": null,
"line": 24,
"name": "docstring found for class 'LongWithShortDocstring' (6 lines) was too short (10 characters)",
"name": "docstring found for class 'LongWithShortDocstring' (6 lines) was too short (10 characters, needed 16)",
"original": null,
"path": "tools/test/docstring_linter_testdata/python_code.py.txt",
"replacement": null,
@ -26,7 +26,7 @@
"code": "DOCSTRING_LINTER",
"description": null,
"line": 71,
"name": "No docstring found for function 'needs_docs' (12 lines)",
"name": "No docstring found for function 'needs_docs' (12 lines). If the method overrides a method on a parent class, adding the `@typing_extensions.override` decorator will make this error go away.",
"original": null,
"path": "tools/test/docstring_linter_testdata/python_code.py.txt",
"replacement": null,
@ -38,9 +38,9 @@
"description": null,
"line": null,
"name": "Suggested fixes for docstring_linter",
"original": "class ShortWithDocstring:\n \"\"\"This docstring, while short, is enough\"\"\"\n pass\n\n\nclass Short:\n pass\n\n\nclass LongWithDocstring:\n \"\"\"This docstring, while short, is enough\"\"\"\n\n def short1(self):\n pass\n\n\nclass LongWithoutDocstring:\n # A comment isn't a docstring\n\n def short1(self):\n pass\n\n\nclass LongWithShortDocstring:\n \"\"\"TODO\"\"\"\n\n def short1(self):\n pass\n\n\nclass _Protected:\n \"\"\"TODO\"\"\"\n\n def short1(self):\n pass\n\n\ndef short():\n #\n #\n #\n pass\n\n\ndef long():\n \"\"\"This docstring, while short, is enough\"\"\"\n #\n #\n #\n #\n pass\n\n\ndef long_without_docstring():\n #\n #\n #\n #\n pass\n\n\nclass ImpossibleCombo(\n set,\n tuple,\n int,\n):\n # We could have comments\n # before the doc comment\n \"\"\"This docstring, while short, is enough\"\"\"\n\n def needs_docs(self):\n def not_short():\n class Long:\n a = 1\n b = 1\n c = 1\n d = 1\n e = 1\n\n class Short:\n pass\n\n\nclass NotDocstring:\n def short1(self):\n pass\n\n \"\"\"This is not a docstring\"\"\"\n\n def short2(self):\n pass\n\n def short3(self):\n pass\n\n\ndef long_with_omit(): # noqa: docstring_linter\n #\n #\n #\n #\n pass\n",
"original": "class ShortWithDocstring:\n \"\"\"This docstring, while short, is enough\"\"\"\n pass\n\n\nclass Short:\n pass\n\n\nclass LongWithDocstring:\n \"\"\"This docstring, while short, is enough\"\"\"\n\n def short1(self):\n pass\n\n\nclass LongWithoutDocstring:\n # A comment isn't a docstring\n\n def short1(self):\n pass\n\n\nclass LongWithShortDocstring:\n \"\"\"TODO\"\"\"\n\n def short1(self):\n pass\n\n\nclass _Protected:\n \"\"\"TODO\"\"\"\n\n def short1(self):\n pass\n\n\ndef short():\n #\n #\n #\n pass\n\n\ndef long():\n \"\"\"This docstring, while short, is enough\"\"\"\n #\n #\n #\n #\n pass\n\n\ndef long_without_docstring():\n #\n #\n #\n #\n pass\n\n\nclass ImpossibleCombo(\n set,\n tuple,\n int,\n):\n # We could have comments\n # before the doc comment\n \"\"\"This docstring, while short, is enough\"\"\"\n\n def needs_docs(self):\n def not_short():\n class Long:\n a = 1\n b = 1\n c = 1\n d = 1\n e = 1\n\n class Short:\n pass\n\n\n@override # Won't work!\nclass NotDocstring:\n def short1(self):\n pass\n\n \"\"\"This is not a docstring\"\"\"\n\n @override\n def long_with_override(self):\n #\n #\n #\n #\n pass\n\n def short2(self):\n pass\n\n def short3(self):\n pass\n\n\n\ndef long_with_omit(): # noqa: docstring_linter\n #\n #\n #\n #\n pass\n",
"path": "tools/test/docstring_linter_testdata/python_code.py.txt",
"replacement": "class ShortWithDocstring:\n \"\"\"This docstring, while short, is enough\"\"\"\n pass\n\n\nclass Short:\n pass\n\n\nclass LongWithDocstring:\n \"\"\"This docstring, while short, is enough\"\"\"\n\n def short1(self):\n pass\n\n\nclass LongWithoutDocstring:\n # A comment isn't a docstring\n\n def short1(self):\n pass\n\n\nclass LongWithShortDocstring:\n \"\"\"TODO\"\"\"\n\n def short1(self):\n pass\n\n\nclass _Protected:\n \"\"\"TODO\"\"\"\n\n def short1(self):\n pass\n\n\ndef short():\n #\n #\n #\n pass\n\n\ndef long():\n \"\"\"This docstring, while short, is enough\"\"\"\n #\n #\n #\n #\n pass\n\n\ndef long_without_docstring():\n #\n #\n #\n #\n pass\n\n\nclass ImpossibleCombo(\n set,\n tuple,\n int,\n):\n # We could have comments\n # before the doc comment\n \"\"\"This docstring, while short, is enough\"\"\"\n\n def needs_docs(self):\n def not_short():\n class Long:\n a = 1\n b = 1\n c = 1\n d = 1\n e = 1\n\n class Short:\n pass\n\n\nclass NotDocstring:\n def short1(self):\n pass\n\n \"\"\"This is not a docstring\"\"\"\n\n def short2(self):\n pass\n\n def short3(self):\n pass\n\n\ndef long_with_omit(): # noqa: docstring_linter\n #\n #\n #\n #\n pass\n",
"replacement": "class ShortWithDocstring:\n \"\"\"This docstring, while short, is enough\"\"\"\n pass\n\n\nclass Short:\n pass\n\n\nclass LongWithDocstring:\n \"\"\"This docstring, while short, is enough\"\"\"\n\n def short1(self):\n pass\n\n\nclass LongWithoutDocstring:\n # A comment isn't a docstring\n\n def short1(self):\n pass\n\n\nclass LongWithShortDocstring:\n \"\"\"TODO\"\"\"\n\n def short1(self):\n pass\n\n\nclass _Protected:\n \"\"\"TODO\"\"\"\n\n def short1(self):\n pass\n\n\ndef short():\n #\n #\n #\n pass\n\n\ndef long():\n \"\"\"This docstring, while short, is enough\"\"\"\n #\n #\n #\n #\n pass\n\n\ndef long_without_docstring():\n #\n #\n #\n #\n pass\n\n\nclass ImpossibleCombo(\n set,\n tuple,\n int,\n):\n # We could have comments\n # before the doc comment\n \"\"\"This docstring, while short, is enough\"\"\"\n\n def needs_docs(self):\n def not_short():\n class Long:\n a = 1\n b = 1\n c = 1\n d = 1\n e = 1\n\n class Short:\n pass\n\n\n@override # Won't work!\nclass NotDocstring:\n def short1(self):\n pass\n\n \"\"\"This is not a docstring\"\"\"\n\n @override\n def long_with_override(self):\n #\n #\n #\n #\n pass\n\n def short2(self):\n pass\n\n def short3(self):\n pass\n\n\n\ndef long_with_omit(): # noqa: docstring_linter\n #\n #\n #\n #\n pass\n",
"severity": "error"
}
]

View File

@ -6,7 +6,7 @@ tools/test/docstring_linter_testdata/python_code.py.txt:17: No docstring found f
18 | # A comment isn't a docstring
19 |
tools/test/docstring_linter_testdata/python_code.py.txt:24: docstring found for class 'LongWithShortDocstring' (6 lines) was too short (10 characters)
tools/test/docstring_linter_testdata/python_code.py.txt:24: docstring found for class 'LongWithShortDocstring' (6 lines) was too short (10 characters, needed 16)
22 |
23 |
24 | class LongWithShortDocstring:
@ -14,7 +14,7 @@ tools/test/docstring_linter_testdata/python_code.py.txt:24: docstring found for
25 | """TODO"""
26 |
tools/test/docstring_linter_testdata/python_code.py.txt:71: No docstring found for function 'needs_docs' (12 lines)
tools/test/docstring_linter_testdata/python_code.py.txt:71: No docstring found for function 'needs_docs' (12 lines). If the method overrides a method on a parent class, adding the `@typing_extensions.override` decorator will make this error go away.
69 | """This docstring, while short, is enough"""
70 |
71 | def needs_docs(self):

View File

@ -2,6 +2,7 @@
{
"category": "class",
"children": [],
"decorators": [],
"display_name": "class ShortWithDocstring",
"docstring": "\"\"\"This docstring, while short, is enough\"\"\"",
"full_name": "ShortWithDocstring",
@ -15,6 +16,7 @@
{
"category": "class",
"children": [],
"decorators": [],
"display_name": "class Short",
"docstring": "",
"full_name": "Short",
@ -31,6 +33,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def LongWithDocstring.short1()",
"docstring": "",
"full_name": "LongWithDocstring.short1",
@ -42,6 +45,7 @@
"start_line": 14
}
],
"decorators": [],
"display_name": "class LongWithDocstring",
"docstring": "\"\"\"This docstring, while short, is enough\"\"\"",
"full_name": "LongWithDocstring",
@ -58,6 +62,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def LongWithoutDocstring.short1()",
"docstring": "",
"full_name": "LongWithoutDocstring.short1",
@ -69,6 +74,7 @@
"start_line": 21
}
],
"decorators": [],
"display_name": "class LongWithoutDocstring",
"docstring": "",
"full_name": "LongWithoutDocstring",
@ -85,6 +91,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def LongWithShortDocstring.short1()",
"docstring": "",
"full_name": "LongWithShortDocstring.short1",
@ -96,6 +103,7 @@
"start_line": 28
}
],
"decorators": [],
"display_name": "class LongWithShortDocstring",
"docstring": "\"\"\"TODO\"\"\"",
"full_name": "LongWithShortDocstring",
@ -112,6 +120,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def _Protected.short1()",
"docstring": "",
"full_name": "_Protected.short1",
@ -123,6 +132,7 @@
"start_line": 35
}
],
"decorators": [],
"display_name": "class _Protected",
"docstring": "\"\"\"TODO\"\"\"",
"full_name": "_Protected",
@ -136,6 +146,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def short()",
"docstring": "",
"full_name": "short",
@ -149,6 +160,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def long()",
"docstring": "\"\"\"This docstring, while short, is enough\"\"\"",
"full_name": "long",
@ -162,6 +174,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def long_without_docstring()",
"docstring": "",
"full_name": "long_without_docstring",
@ -184,6 +197,7 @@
{
"category": "class",
"children": [],
"decorators": [],
"display_name": "class ImpossibleCombo.needs_docs.not_short.Long",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs.not_short.Long",
@ -197,6 +211,7 @@
{
"category": "class",
"children": [],
"decorators": [],
"display_name": "class ImpossibleCombo.needs_docs.not_short.Short",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs.not_short.Short",
@ -208,6 +223,7 @@
"start_line": 81
}
],
"decorators": [],
"display_name": "def ImpossibleCombo.needs_docs.not_short()",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs.not_short",
@ -221,6 +237,7 @@
{
"category": "class",
"children": [],
"decorators": [],
"display_name": "class ImpossibleCombo.needs_docs.not_short.Long",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs.not_short.Long",
@ -234,6 +251,7 @@
{
"category": "class",
"children": [],
"decorators": [],
"display_name": "class ImpossibleCombo.needs_docs.not_short.Short",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs.not_short.Short",
@ -245,6 +263,7 @@
"start_line": 81
}
],
"decorators": [],
"display_name": "def ImpossibleCombo.needs_docs()",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs",
@ -261,6 +280,7 @@
{
"category": "class",
"children": [],
"decorators": [],
"display_name": "class ImpossibleCombo.needs_docs.not_short.Long",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs.not_short.Long",
@ -274,6 +294,7 @@
{
"category": "class",
"children": [],
"decorators": [],
"display_name": "class ImpossibleCombo.needs_docs.not_short.Short",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs.not_short.Short",
@ -285,6 +306,7 @@
"start_line": 81
}
],
"decorators": [],
"display_name": "def ImpossibleCombo.needs_docs.not_short()",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs.not_short",
@ -298,6 +320,7 @@
{
"category": "class",
"children": [],
"decorators": [],
"display_name": "class ImpossibleCombo.needs_docs.not_short.Long",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs.not_short.Long",
@ -311,6 +334,7 @@
{
"category": "class",
"children": [],
"decorators": [],
"display_name": "class ImpossibleCombo.needs_docs.not_short.Short",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs.not_short.Short",
@ -322,6 +346,7 @@
"start_line": 81
}
],
"decorators": [],
"display_name": "class ImpossibleCombo",
"docstring": "\"\"\"This docstring, while short, is enough\"\"\"",
"full_name": "ImpossibleCombo",
@ -338,6 +363,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def NotDocstring.short1()",
"docstring": "",
"full_name": "NotDocstring.short1",
@ -346,56 +372,78 @@
"is_method": true,
"line_count": 2,
"parent": 18,
"start_line": 86
"start_line": 87
},
{
"category": "def",
"children": [],
"display_name": "def NotDocstring.short2()",
"decorators": [
"@override"
],
"display_name": "def NotDocstring.long_with_override()",
"docstring": "",
"full_name": "NotDocstring.short2",
"full_name": "NotDocstring.long_with_override",
"index": 20,
"is_local": false,
"is_method": true,
"line_count": 2,
"parent": 18,
"start_line": 91
"start_line": 97
},
{
"category": "def",
"children": [],
"display_name": "def NotDocstring.short3()",
"decorators": [],
"display_name": "def NotDocstring.short2()",
"docstring": "",
"full_name": "NotDocstring.short3",
"full_name": "NotDocstring.short2",
"index": 21,
"is_local": false,
"is_method": true,
"line_count": 3,
"line_count": 2,
"parent": 18,
"start_line": 94
"start_line": 100
},
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def NotDocstring.short3()",
"docstring": "",
"full_name": "NotDocstring.short3",
"index": 22,
"is_local": false,
"is_method": true,
"line_count": 4,
"parent": 18,
"start_line": 103
}
],
"decorators": [
"@override"
],
"display_name": "class NotDocstring",
"docstring": "",
"full_name": "NotDocstring",
"index": 18,
"is_local": false,
"is_method": false,
"line_count": 12,
"line_count": 21,
"parent": null,
"start_line": 85
"start_line": 86
},
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def long_with_omit()",
"docstring": "",
"full_name": "long_with_omit",
"index": 22,
"index": 23,
"is_local": false,
"is_method": false,
"line_count": 1,
"parent": null,
"start_line": 102
"start_line": 112
}
]

View File

@ -123,28 +123,34 @@
},
"class NotDocstring": {
"children": {
"86": {
" 87": {
"docstring_len": 0,
"lines": 2,
"name": "def NotDocstring.short1",
"status": "good"
},
"91": {
" 97": {
"docstring_len": 0,
"lines": 2,
"name": "def NotDocstring.long_with_override",
"status": "good"
},
"100": {
"docstring_len": 0,
"lines": 2,
"name": "def NotDocstring.short2",
"status": "good"
},
"94": {
"103": {
"docstring_len": 0,
"lines": 3,
"lines": 4,
"name": "def NotDocstring.short3",
"status": "good"
}
},
"docstring_len": 0,
"line": 85,
"lines": 12,
"line": 86,
"lines": 21,
"status": "good"
},
"class Short": {
@ -181,7 +187,7 @@
},
"def long_with_omit": {
"docstring_len": 0,
"line": 102,
"line": 112,
"lines": 1,
"status": "good"
},

View File

@ -165,33 +165,39 @@
"name": "class ImpossibleCombo",
"status": "good"
},
" 85": {
" 86": {
"children": {
"86": {
" 87": {
"docstring_len": 0,
"lines": 2,
"name": "def NotDocstring.short1",
"status": "good"
},
"91": {
" 97": {
"docstring_len": 0,
"lines": 2,
"name": "def NotDocstring.long_with_override",
"status": "good"
},
"100": {
"docstring_len": 0,
"lines": 2,
"name": "def NotDocstring.short2",
"status": "good"
},
"94": {
"103": {
"docstring_len": 0,
"lines": 3,
"lines": 4,
"name": "def NotDocstring.short3",
"status": "good"
}
},
"docstring_len": 0,
"lines": 12,
"lines": 21,
"name": "class NotDocstring",
"status": "good"
},
"102": {
"112": {
"docstring_len": 0,
"lines": 1,
"name": "def long_with_omit",

View File

@ -2,6 +2,7 @@
{
"category": "class",
"children": [],
"decorators": [],
"display_name": "class ShortWithDocstring",
"docstring": "\"\"\"This docstring, while short, is enough\"\"\"",
"full_name": "ShortWithDocstring",
@ -15,6 +16,7 @@
{
"category": "class",
"children": [],
"decorators": [],
"display_name": "class Short",
"docstring": "",
"full_name": "Short",
@ -30,6 +32,7 @@
"children": [
3
],
"decorators": [],
"display_name": "class LongWithDocstring",
"docstring": "\"\"\"This docstring, while short, is enough\"\"\"",
"full_name": "LongWithDocstring",
@ -43,6 +46,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def LongWithDocstring.short1()",
"docstring": "",
"full_name": "LongWithDocstring.short1",
@ -58,6 +62,7 @@
"children": [
5
],
"decorators": [],
"display_name": "class LongWithoutDocstring",
"docstring": "",
"full_name": "LongWithoutDocstring",
@ -71,6 +76,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def LongWithoutDocstring.short1()",
"docstring": "",
"full_name": "LongWithoutDocstring.short1",
@ -86,6 +92,7 @@
"children": [
7
],
"decorators": [],
"display_name": "class LongWithShortDocstring",
"docstring": "\"\"\"TODO\"\"\"",
"full_name": "LongWithShortDocstring",
@ -99,6 +106,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def LongWithShortDocstring.short1()",
"docstring": "",
"full_name": "LongWithShortDocstring.short1",
@ -114,6 +122,7 @@
"children": [
9
],
"decorators": [],
"display_name": "class _Protected",
"docstring": "\"\"\"TODO\"\"\"",
"full_name": "_Protected",
@ -127,6 +136,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def _Protected.short1()",
"docstring": "",
"full_name": "_Protected.short1",
@ -140,6 +150,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def short()",
"docstring": "",
"full_name": "short",
@ -153,6 +164,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def long()",
"docstring": "\"\"\"This docstring, while short, is enough\"\"\"",
"full_name": "long",
@ -166,6 +178,7 @@
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def long_without_docstring()",
"docstring": "",
"full_name": "long_without_docstring",
@ -184,6 +197,7 @@
16,
17
],
"decorators": [],
"display_name": "class ImpossibleCombo",
"docstring": "\"\"\"This docstring, while short, is enough\"\"\"",
"full_name": "ImpossibleCombo",
@ -201,6 +215,7 @@
16,
17
],
"decorators": [],
"display_name": "def ImpossibleCombo.needs_docs()",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs",
@ -217,6 +232,7 @@
16,
17
],
"decorators": [],
"display_name": "def ImpossibleCombo.needs_docs.not_short()",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs.not_short",
@ -230,6 +246,7 @@
{
"category": "class",
"children": [],
"decorators": [],
"display_name": "class ImpossibleCombo.needs_docs.not_short.Long",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs.not_short.Long",
@ -243,6 +260,7 @@
{
"category": "class",
"children": [],
"decorators": [],
"display_name": "class ImpossibleCombo.needs_docs.not_short.Short",
"docstring": "",
"full_name": "ImpossibleCombo.needs_docs.not_short.Short",
@ -258,7 +276,11 @@
"children": [
19,
20,
21
21,
22
],
"decorators": [
"@override"
],
"display_name": "class NotDocstring",
"docstring": "",
@ -266,13 +288,14 @@
"index": 18,
"is_local": false,
"is_method": false,
"line_count": 12,
"line_count": 21,
"parent": null,
"start_line": 85
"start_line": 86
},
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def NotDocstring.short1()",
"docstring": "",
"full_name": "NotDocstring.short1",
@ -281,45 +304,64 @@
"is_method": true,
"line_count": 2,
"parent": 18,
"start_line": 86
"start_line": 87
},
{
"category": "def",
"children": [],
"display_name": "def NotDocstring.short2()",
"decorators": [
"@override"
],
"display_name": "def NotDocstring.long_with_override()",
"docstring": "",
"full_name": "NotDocstring.short2",
"full_name": "NotDocstring.long_with_override",
"index": 20,
"is_local": false,
"is_method": true,
"line_count": 2,
"parent": 18,
"start_line": 91
"start_line": 97
},
{
"category": "def",
"children": [],
"display_name": "def NotDocstring.short3()",
"decorators": [],
"display_name": "def NotDocstring.short2()",
"docstring": "",
"full_name": "NotDocstring.short3",
"full_name": "NotDocstring.short2",
"index": 21,
"is_local": false,
"is_method": true,
"line_count": 3,
"line_count": 2,
"parent": 18,
"start_line": 94
"start_line": 100
},
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def NotDocstring.short3()",
"docstring": "",
"full_name": "NotDocstring.short3",
"index": 22,
"is_local": false,
"is_method": true,
"line_count": 4,
"parent": 18,
"start_line": 103
},
{
"category": "def",
"children": [],
"decorators": [],
"display_name": "def long_with_omit()",
"docstring": "",
"full_name": "long_with_omit",
"index": 22,
"index": 23,
"is_local": false,
"is_method": false,
"line_count": 1,
"parent": null,
"start_line": 102
"start_line": 112
}
]

View File

@ -17,9 +17,10 @@
" 73": "def ImpossibleCombo.needs_docs.not_short(): lines=11, docs=0",
" 74": "class ImpossibleCombo.needs_docs.not_short.Long: lines=6, docs=0",
" 81": "class ImpossibleCombo.needs_docs.not_short.Short: lines=3, docs=0",
" 85": "class NotDocstring: lines=12, docs=0",
" 86": "def NotDocstring.short1(): lines=2, docs=0",
" 91": "def NotDocstring.short2(): lines=2, docs=0",
" 94": "def NotDocstring.short3(): lines=3, docs=0",
"102": "def long_with_omit(): lines=1, docs=0"
" 86": "class NotDocstring: lines=21, docs=0",
" 87": "def NotDocstring.short1(): lines=2, docs=0",
" 97": "def NotDocstring.long_with_override(): lines=2, docs=0",
"100": "def NotDocstring.short2(): lines=2, docs=0",
"103": "def NotDocstring.short3(): lines=4, docs=0",
"112": "def long_with_omit(): lines=1, docs=0"
}

View File

@ -37,8 +37,8 @@
},
"class NotDocstring": {
"docstring_len": 0,
"line": 85,
"lines": 12,
"line": 86,
"lines": 21,
"status": "good"
},
"class Short": {
@ -89,22 +89,28 @@
"lines": 3,
"status": "good"
},
"def NotDocstring.long_with_override": {
"docstring_len": 0,
"line": 97,
"lines": 2,
"status": "good"
},
"def NotDocstring.short1": {
"docstring_len": 0,
"line": 86,
"line": 87,
"lines": 2,
"status": "good"
},
"def NotDocstring.short2": {
"docstring_len": 0,
"line": 91,
"line": 100,
"lines": 2,
"status": "good"
},
"def NotDocstring.short3": {
"docstring_len": 0,
"line": 94,
"lines": 3,
"line": 103,
"lines": 4,
"status": "good"
},
"def _Protected.short1": {
@ -121,7 +127,7 @@
},
"def long_with_omit": {
"docstring_len": 0,
"line": 102,
"line": 112,
"lines": 1,
"status": "good"
},

View File

@ -107,31 +107,37 @@
"name": "class ImpossibleCombo.needs_docs.not_short.Short",
"status": "good"
},
" 85": {
" 86": {
"docstring_len": 0,
"lines": 12,
"lines": 21,
"name": "class NotDocstring",
"status": "good"
},
" 86": {
" 87": {
"docstring_len": 0,
"lines": 2,
"name": "def NotDocstring.short1",
"status": "good"
},
" 91": {
" 97": {
"docstring_len": 0,
"lines": 2,
"name": "def NotDocstring.long_with_override",
"status": "good"
},
"100": {
"docstring_len": 0,
"lines": 2,
"name": "def NotDocstring.short2",
"status": "good"
},
" 94": {
"103": {
"docstring_len": 0,
"lines": 3,
"lines": 4,
"name": "def NotDocstring.short3",
"status": "good"
},
"102": {
"112": {
"docstring_len": 0,
"lines": 1,
"name": "def long_with_omit",

View File

@ -1,6 +1,7 @@
# mypy: ignore-errors
import io
import itertools
import json
import sys
import tempfile
@ -8,6 +9,7 @@ from pathlib import Path
from unittest import mock
from tools.linter.adapters.docstring_linter import (
_get_decorators,
DocstringLinter,
file_summary,
make_recursive,
@ -119,13 +121,26 @@ class TestDocstringLinter(LinterTestCase):
]
self.assertEqual(actual, expected)
def test_decorators(self):
tests = itertools.product(INDENTS, DECORATORS.items())
for indent, (name, (expected, test_inputs)) in tests:
ind = indent * " "
for data in test_inputs:
prog = "".join(ind + d + "\n" for d in data)
pf = DocstringLinter.make_file(prog)
it = (i for i, t in enumerate(pf.tokens) if t.string == "def")
def_t = next(it, 0)
with self.subTest("Decorator", indent=indent, name=name, data=data):
actual = list(_get_decorators(pf.tokens, def_t))
self.assertEqual(actual, expected)
def _dumps(d: dict) -> str:
return json.dumps(d, sort_keys=True, indent=2) + "\n"
def _data():
docstring_file = DocstringLinter.make_file(TEST_FILE)
def _data(file=TEST_FILE):
docstring_file = DocstringLinter.make_file(file)
return [b.as_data() for b in docstring_file.blocks]
@ -135,3 +150,54 @@ def _next_stdout(mock_stdout):
s = mock_stdout.getvalue()
yield s[length:]
length = len(s)
CONSTANT = "A = 10"
COMMENT = "# a simple function"
OVER = "@override"
WRAPS = "@functools.wraps(fn)"
MASSIVE = (
"@some.long.path.very_long_function_name(",
" adjust_something_fiddly=1231232,",
" disable_something_critical=True,)",
)
MASSIVE_FLAT = (
"@some.long.path.very_long_function_name("
"adjust_something_fiddly=1231232,"
"disable_something_critical=True,)"
)
DEF = "def function():", " pass"
INDENTS = 0, 4, 8
DECORATORS = {
"none": (
[],
(
[],
[*DEF],
[COMMENT, *DEF],
[CONSTANT, "", COMMENT, *DEF],
[OVER, CONSTANT, *DEF], # Probably not even Python. :-)
),
),
"one": (
[OVER],
(
[OVER, *DEF],
[OVER, COMMENT, *DEF],
[OVER, COMMENT, "", *DEF],
[COMMENT, OVER, "", COMMENT, "", *DEF],
),
),
"two": (
[OVER, WRAPS],
(
[OVER, WRAPS, *DEF],
[COMMENT, OVER, COMMENT, WRAPS, COMMENT, *DEF],
),
),
"massive": (
[MASSIVE_FLAT, OVER],
([*MASSIVE, OVER, *DEF],),
),
}