Skip to content

Commit

Permalink
docparams extension considers type comments as type documentation.
Browse files Browse the repository at this point in the history
Closes #6287
  • Loading branch information
AWhetter committed Oct 14, 2022
1 parent a6cb836 commit 41593ed
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 10 deletions.
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ Release date: TBA

Closes #4525

* docparams extension considers type comments as type documentation.

Closes #6287

* Fix false positive for ``unsubscriptable-object`` in Python 3.8 and below for
statements guarded by ``if TYPE_CHECKING``.

Expand Down
4 changes: 4 additions & 0 deletions doc/whatsnew/2.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,7 @@ Other Changes
attribute to itself without any prior assignment.

Closes #1555

* docparams extension considers type comments as type documentation.

Closes #6287
105 changes: 105 additions & 0 deletions pylint/extensions/_check_docs_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

"""Utility methods for docstring checking."""

import itertools
import re
from typing import List, Optional, Set, Tuple

import astroid
from astroid import nodes

from pylint.checkers import utils
from pylint.constants import PY38_PLUS


def space_indentation(s):
Expand Down Expand Up @@ -156,6 +158,109 @@ def possible_exc_types(node: nodes.NodeNG) -> Set[nodes.ClassDef]:
return set()


def _is_ellipsis(node: astroid.NodeNG) -> bool:
if not PY38_PLUS:
return isinstance(node, astroid.Ellipsis)

return isinstance(node, astroid.Const) and node.value == Ellipsis


def _merge_annotations(annotations, comment_annotations):
for ann, comment_ann in itertools.zip_longest(annotations, comment_annotations):
if ann and not _is_ellipsis(ann):
yield ann
elif comment_ann and not _is_ellipsis(comment_ann):
yield comment_ann
else:
yield None


def _annotations_list(args_node: astroid.Arguments) -> List[astroid.NodeNG]:
"""Get a merged list of annotations.
The annotations can come from:
* Real type annotations.
* A type comment on the function.
* A type common on the individual argument.
:param args_node: The node to get the annotations for.
:returns: The annotations.
"""
plain_annotations = args_node.annotations or ()
func_comment_annotations = args_node.parent.type_comment_args or ()
comment_annotations = args_node.type_comment_posonlyargs
comment_annotations += args_node.type_comment_args or []
comment_annotations += args_node.type_comment_kwonlyargs
return list(
_merge_annotations(
plain_annotations,
_merge_annotations(func_comment_annotations, comment_annotations),
)
)


def args_with_annotation(args_node: astroid.Arguments) -> Set[str]:
result = set()
annotations = _annotations_list(args_node)
annotation_offset = 0

if args_node.posonlyargs:
posonlyargs_annotations = args_node.posonlyargs_annotations
if not any(args_node.posonlyargs_annotations):
num_args = len(args_node.posonlyargs)
posonlyargs_annotations = annotations[
annotation_offset : annotation_offset + num_args
]
annotation_offset += num_args

for arg, annotation in zip(args_node.posonlyargs, posonlyargs_annotations):
if annotation:
result.add(arg.name)

if args_node.args:
num_args = len(args_node.args)
for arg, annotation in zip(
args_node.args,
annotations[annotation_offset : annotation_offset + num_args],
):
if annotation:
result.add(arg.name)

annotation_offset += num_args

if args_node.vararg:
annotation = None
if args_node.varargannotation:
result.add(args_node.vararg)
elif len(annotations) > annotation_offset and annotations[annotation_offset]:
result.add(args_node.vararg)
annotation_offset += 1

if args_node.kwonlyargs:
kwonlyargs_annotations = args_node.kwonlyargs_annotations
if not any(args_node.kwonlyargs_annotations):
num_args = len(args_node.kwonlyargs)
kwonlyargs_annotations = annotations[
annotation_offset : annotation_offset + num_args
]
annotation_offset += num_args

for arg, annotation in zip(args_node.kwonlyargs, kwonlyargs_annotations):
if annotation:
result.add(arg.name)

if args_node.kwarg:
annotation = None
if args_node.kwargannotation:
result.add(args_node.kwarg)
elif len(annotations) > annotation_offset and annotations[annotation_offset]:
result.add(args_node.kwarg)
annotation_offset += 1

return result


def docstringify(
docstring: Optional[nodes.Const], default_type: str = "default"
) -> "Docstring":
Expand Down
19 changes: 9 additions & 10 deletions pylint/extensions/docparams.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ def visit_return(self, node: nodes.Return) -> None:
if not (doc.has_returns() or (doc.has_property_returns() and is_property)):
self.add_message("missing-return-doc", node=func_node)

if func_node.returns:
if func_node.returns or func_node.type_comment_returns:
return

if not (doc.has_rtype() or (doc.has_property_type() and is_property)):
Expand Down Expand Up @@ -358,7 +358,9 @@ def visit_yield(self, node: nodes.Yield) -> None:
if not doc_has_yields:
self.add_message("missing-yield-doc", node=func_node)

if not (doc_has_yields_type or func_node.returns):
if not (
doc_has_yields_type or func_node.returns or func_node.type_comment_returns
):
self.add_message("missing-yield-type-doc", node=func_node)

def visit_yieldfrom(self, node: nodes.YieldFrom) -> None:
Expand Down Expand Up @@ -524,7 +526,9 @@ class constructor.

# Collect the function arguments.
expected_argument_names = {arg.name for arg in arguments_node.args}
expected_argument_names.update(arg.name for arg in arguments_node.kwonlyargs)
expected_argument_names.update(
a.name for a in arguments_node.posonlyargs + arguments_node.kwonlyargs
)
not_needed_type_in_docstring = self.not_needed_param_in_docstring.copy()

expected_but_ignored_argument_names = set()
Expand All @@ -547,7 +551,7 @@ class constructor.
if not params_with_doc and not params_with_type and accept_no_param_doc:
tolerate_missing_params = True

# This is before the update of param_with_type because this must check only
# This is before the update of params_with_type because this must check only
# the type documented in a docstring, not the one using pep484
# See #4117 and #4593
self._compare_ignored_args(
Expand All @@ -556,12 +560,7 @@ class constructor.
expected_but_ignored_argument_names,
warning_node,
)
for index, arg_name in enumerate(arguments_node.args):
if arguments_node.annotations[index]:
params_with_type.add(arg_name.name)
for index, arg_name in enumerate(arguments_node.kwonlyargs):
if arguments_node.kwonlyargs_annotations[index]:
params_with_type.add(arg_name.name)
params_with_type |= utils.args_with_annotation(arguments_node)

if not tolerate_missing_params:
missing_param_doc = (expected_argument_names - params_with_doc) - (
Expand Down
51 changes: 51 additions & 0 deletions tests/functional/ext/docparams/missing_param_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,54 @@ def foobar15(*args):
Relevant parameters.
"""
print(args)


def foobar16(one: int, two: str, three: float) -> int:
"""Description of the function
Args:
one: A number.
two: Another number.
three: Yes another number.
Returns:
The number one.
"""
print(one, two, three)
return 1


def foobar17(one, two, three):
# type: (int, str, float) -> int
"""Description of the function
Args:
one: A number.
two: Another number.
three: Yes another number.
Returns:
The number one.
"""
print(one, two, three)
return 1


def foobar18(
one, # type: int
two, # type: str
three, # type: float
):
# type: (...) -> int
"""Description of the function
Args:
one: A number.
two: Another number.
three: Yes another number.
Returns:
The number one.
"""
print(one, two, three)
return 1
53 changes: 53 additions & 0 deletions tests/functional/ext/docparams/missing_param_doc_py38.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#pylint: disable= missing-module-docstring

def foobar1(one: int, /, two: str, *, three: float) -> int:
"""Description of the function
Args:
one: A number.
two: Another number.
three: Yes another number.
Returns:
The number one.
"""
print(one, two, three)
return 1


def foobar2(one, /, two, * three):
# type: (int, str, float) -> int
"""Description of the function
Args:
one: A number.
two: Another number.
three: Yes another number.
Returns:
The number one.
"""
print(one, two, three)
return 1


def foobar3(
one, # type: int
/,
two, # type: str
*,
three, # type: float
):
# type: (...) -> int
"""Description of the function
Args:
one: A number.
two: Another number.
three: Yes another number.
Returns:
The number one.
"""
print(one, two, three)
return 1
11 changes: 11 additions & 0 deletions tests/functional/ext/docparams/missing_param_doc_py38.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[MASTER]
load-plugins=pylint.extensions.docparams,

[testoptions]
min_pyver=3.8

[PARAMETER_DOCUMENTATION]
accept-no-param-doc=no
accept-no-raise-doc=no
accept-no-return-doc=no
accept-no-yields-doc=no

0 comments on commit 41593ed

Please sign in to comment.