Skip to content

Commit

Permalink
Allow no return section in docstring (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsh9 authored May 29, 2023
1 parent a1fda04 commit 1f0485d
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 6 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## Unreleased

- Added
- A new option to allow no return section in the docstring if the function
implicitly returns `None`

## [0.0.4] - 2023-05-27

- Added
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ flake8 --style=google <FILE_OR_FOLDER>
If `True`, the type hints in the docstring and in the Python code need to
exactly match.

To turn this optoin on/off, do this:
To turn this option on/off, do this:

```
pydoclint --check-type-hint=False <FILE_OR_FOLDER>
Expand All @@ -201,7 +201,7 @@ flake8 --check-type-hint=False <FILE_OR_FOLDER>
If `True`, the input argument order in the docstring needs to match that in the
function signature.

To turn this optoin on/off, do this:
To turn this option on/off, do this:

```
pydoclint --check-arg-order=False <FILE_OR_FOLDER>
Expand All @@ -218,7 +218,7 @@ flake8 --check-arg-order=False <FILE_OR_FOLDER>
If `True`, `pydoclint` won't check functions that have only a short description
in their docstring.

To turn this optoin on/off, do this:
To turn this option on/off, do this:

```
pydoclint --skip-checking-short-docstrings=False <FILE_OR_FOLDER>
Expand All @@ -241,3 +241,9 @@ docstring (or vice versa).
If it is set to `True`, having a docstring for class constructors
(`__init__()`) is allowed, and the arguments are expected to be documented
under `__init__()` rather than in the class docstring.

### 4.9. `--require-return-section-when-returning-none` (shortform: `-rrs`, default: `False`)

If `False`, a "return" section is not necessary in the docstring if the
function implicitly returns `None` (for example, doesn't have a return
statement, or has `-> None` as the return annotation).
19 changes: 19 additions & 0 deletions pydoclint/flake8_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ def add_options(cls, parser): # noqa: D102
default='False',
help='If True, allow both __init__() and the class def to have docstrings',
)
parser.add_option(
'-rrs',
'--require-return-section-when-returning-none',
action='store',
default='False',
help=(
'If False, a return section is not needed in docstring if'
' the function body does not have a "return" statement and'
' the return type annotation is "-> None".'
),
)

@classmethod
def parse_options(cls, options): # noqa: D102
Expand All @@ -70,6 +81,9 @@ def parse_options(cls, options): # noqa: D102
)
cls.skip_checking_raises = options.skip_checking_raises
cls.allow_init_docstring = options.allow_init_docstring
cls.require_return_section_when_returning_none = (
options.require_return_section_when_returning_none
)
cls.style = options.style

def run(self) -> Generator[Tuple[int, int, str, Any], None, None]:
Expand All @@ -88,6 +102,10 @@ def run(self) -> Generator[Tuple[int, int, str, Any], None, None]:
'--allow-init-docstring',
self.allow_init_docstring,
)
requireReturnSectionWhenReturningNone = self._bool(
'--require-return-section-when-returning-none',
self.require_return_section_when_returning_none,
)

if self.style not in {'numpy', 'google'}:
raise ValueError(
Expand All @@ -100,6 +118,7 @@ def run(self) -> Generator[Tuple[int, int, str, Any], None, None]:
skipCheckingShortDocstrings=skipCheckingShortDocstrings,
skipCheckingRaises=skipCheckingRaises,
allowInitDocstring=allowInitDocstring,
requireReturnSectionWhenReturningNone=requireReturnSectionWhenReturningNone,
style=self.style,
)
v.visit(self._tree)
Expand Down
18 changes: 18 additions & 0 deletions pydoclint/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ def validateStyleValue(
default=False,
help='If True, allow both __init__() and the class def to have docstrings',
)
@click.option(
'-rrs',
'--require-return-section-when-returning-none',
type=bool,
show_default=True,
default=False,
help=(
'If False, a return section is not needed in docstring if'
' the function body does not have a "return" statement and'
' the return type annotation is "-> None".'
),
)
@click.argument(
'paths',
nargs=-1,
Expand All @@ -124,6 +136,7 @@ def main(
skip_checking_short_docstrings: bool,
skip_checking_raises: bool,
allow_init_docstring: bool,
require_return_section_when_returning_none: bool,
) -> None:
"""Command-line entry point of pydoclint"""
ctx.ensure_object(dict)
Expand Down Expand Up @@ -151,6 +164,7 @@ def main(
skipCheckingShortDocstrings=skip_checking_short_docstrings,
skipCheckingRaises=skip_checking_raises,
allowInitDocstring=allow_init_docstring,
requireReturnSectionWhenReturningNone=require_return_section_when_returning_none,
)

violationCounter: int = 0
Expand Down Expand Up @@ -195,6 +209,7 @@ def _checkPaths(
skipCheckingShortDocstrings: bool = True,
skipCheckingRaises: bool = False,
allowInitDocstring: bool = False,
requireReturnSectionWhenReturningNone: bool = False,
quiet: bool = False,
exclude: str = '',
) -> Dict[str, List[Violation]]:
Expand Down Expand Up @@ -230,6 +245,7 @@ def _checkPaths(
skipCheckingShortDocstrings=skipCheckingShortDocstrings,
skipCheckingRaises=skipCheckingRaises,
allowInitDocstring=allowInitDocstring,
requireReturnSectionWhenReturningNone=requireReturnSectionWhenReturningNone,
)
allViolations[filename.as_posix()] = violationsInThisFile

Expand All @@ -244,6 +260,7 @@ def _checkFile(
skipCheckingShortDocstrings: bool = True,
skipCheckingRaises: bool = False,
allowInitDocstring: bool = False,
requireReturnSectionWhenReturningNone: bool = False,
) -> List[Violation]:
with open(filename) as fp:
src: str = ''.join(fp.readlines())
Expand All @@ -256,6 +273,7 @@ def _checkFile(
skipCheckingShortDocstrings=skipCheckingShortDocstrings,
skipCheckingRaises=skipCheckingRaises,
allowInitDocstring=allowInitDocstring,
requireReturnSectionWhenReturningNone=requireReturnSectionWhenReturningNone,
)
visitor.visit(tree)
return visitor.violations
Expand Down
9 changes: 9 additions & 0 deletions pydoclint/utils/return_yield_raise.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ def hasReturnAnnotation(node: FuncOrAsyncFuncDef) -> bool:
return node.returns is not None


def isReturnAnnotationNone(node: FuncOrAsyncFuncDef) -> bool:
"""Check whether the return type annotation if `None`"""
return _isNone(node.returns)


def hasGeneratorAsReturnAnnotation(node: FuncOrAsyncFuncDef) -> bool:
"""Check whether the function node has a 'Generator' return annotation"""
if node.returns is None:
Expand Down Expand Up @@ -74,3 +79,7 @@ def _hasReturnOrRaiseStatements(
return True

return False


def _isNone(node: ast.AST) -> bool:
return isinstance(node, ast.Constant) and node.value is None
13 changes: 10 additions & 3 deletions pydoclint/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
hasReturnAnnotation,
hasReturnStatements,
hasYieldStatements,
isReturnAnnotationNone,
)
from pydoclint.utils.violation import Violation

Expand All @@ -33,13 +34,17 @@ def __init__(
skipCheckingShortDocstrings: bool = True,
skipCheckingRaises: bool = False,
allowInitDocstring: bool = False,
requireReturnSectionWhenReturningNone: bool = False,
) -> None:
self.style: str = style
self.checkTypeHint: bool = checkTypeHint
self.checkArgOrder: bool = checkArgOrder
self.skipCheckingShortDocstrings: bool = skipCheckingShortDocstrings
self.skipCheckingRaises: bool = skipCheckingRaises
self.allowInitDocstring: bool = allowInitDocstring
self.requireReturnSectionWhenReturningNone: bool = (
requireReturnSectionWhenReturningNone
)

self.parent: Optional[ast.AST] = None # keep track of parent node
self.violations: List[Violation] = []
Expand Down Expand Up @@ -374,9 +379,8 @@ def checkArguments( # noqa: C901

return violations

@classmethod
def checkReturns(
cls,
self,
node: FuncOrAsyncFuncDef,
parent: ast.AST,
doc: Doc,
Expand All @@ -400,7 +404,10 @@ def checkReturns(
# If "Generator[...]" is put in the return type annotation,
# we don't need a "Returns" section in the docstring. Instead,
# we need a "Yields" section.
violations.append(v201)
if self.requireReturnSectionWhenReturningNone:
violations.append(v201)
elif not isReturnAnnotationNone(node):
violations.append(v201)

if docstringHasReturnSection and not (hasReturnStmt or hasReturnAnno):
violations.append(v202)
Expand Down
59 changes: 59 additions & 0 deletions tests/data/google/no_return_section/cases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from typing import Optional


def func1(text: str) -> None:
"""A return section can be omitted if requireReturnSectionWhenReturningNone
is set to False.
Args:
text (str): Text for the function
"""
print(123)


def func2(text: str) -> int:
"""A return section is always required because it returns something
Args:
text (str): Text for the function
"""
return 1


def func3(text: str) -> Optional[int]:
"""A return section is always required because it returns something
Args:
text (str): Text for the function
"""
return 1


def func4(text: str):
"""A return section is always required because it has explicit "return"
in the function body (even if it is "return None")
Args:
text (str): Text for the function
"""
return None


def func5(text: str):
"""A return section is always required because it has explicit "return"
in the function body (even if it is just "return")
Args:
text (str): Text for the function
"""
return


def func6(text: str):
"""A return section is never necessary because it doesn't return
anything and there is no return type annotation.
Args:
text (str): Text for the function
"""
print(123)
71 changes: 71 additions & 0 deletions tests/data/numpy/no_return_section/cases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from typing import Optional


def func1(text: str) -> None:
"""A return section can be omitted if requireReturnSectionWhenReturningNone
is set to False.
Parameters
----------
text : str
Text for the function
"""
print(123)


def func2(text: str) -> int:
"""A return section is always required because it returns something
Parameters
----------
text : str
Text for the function
"""
return 1


def func3(text: str) -> Optional[int]:
"""A return section is always required because it returns something
Parameters
----------
text : str
Text for the function
"""
return 1


def func4(text: str):
"""A return section is always required because it has explicit "return"
in the function body (even if it is "return None")
Parameters
----------
text : str
Text for the function
"""
return None


def func5(text: str):
"""A return section is always required because it has explicit "return"
in the function body (even if it is just "return")
Parameters
----------
text : str
Text for the function
"""
return


def func6(text: str):
"""A return section is never necessary because it doesn't return
anything and there is no return type annotation.
Parameters
----------
text : str
Text for the function
"""
print(123)
Loading

0 comments on commit 1f0485d

Please sign in to comment.