From 8b11d77315ca7a5e15da519db1663d05805dd075 Mon Sep 17 00:00:00 2001 From: Ashwin Vinod Date: Sat, 11 Nov 2023 23:20:21 +0530 Subject: [PATCH] feat: Add option to warn about unknown parameters in Sphinx docstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #64: https://github.com/mkdocstrings/griffe/issues/64 PR #210: https://github.com/mkdocstrings/griffe/pull/210 Co-authored-by: Timothée Mazzucotelli --- src/griffe/docstrings/sphinx.py | 67 ++++++++++++++++++++++++---- tests/test_docstrings/test_sphinx.py | 24 ++++++++++ 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/src/griffe/docstrings/sphinx.py b/src/griffe/docstrings/sphinx.py index f448d51f..cc0873ad 100644 --- a/src/griffe/docstrings/sphinx.py +++ b/src/griffe/docstrings/sphinx.py @@ -85,11 +85,12 @@ class ParsedValues: return_type: str | None = None -def parse(docstring: Docstring, **options: Any) -> list[DocstringSection]: # noqa: ARG001 +def parse(docstring: Docstring, *, warn_unknown_params: bool = True, **options: Any) -> list[DocstringSection]: """Parse a Sphinx-style docstring. Parameters: docstring: The docstring to parse. + warn_unknown_params: Warn about documented parameters not appearing in the signature. **options: Additional parsing options. Returns: @@ -97,6 +98,11 @@ def parse(docstring: Docstring, **options: Any) -> list[DocstringSection]: # no """ parsed_values = ParsedValues() + options = { + "warn_unknown_params": warn_unknown_params, + **options, + } + lines = docstring.lines curr_line_index = 0 @@ -105,7 +111,7 @@ def parse(docstring: Docstring, **options: Any) -> list[DocstringSection]: # no for field_type in field_types: if field_type.matches(line): # https://github.com/python/mypy/issues/5485 - curr_line_index = field_type.reader(docstring, curr_line_index, parsed_values) + curr_line_index = field_type.reader(docstring, curr_line_index, parsed_values, **options) break else: parsed_values.description.append(line) @@ -115,7 +121,14 @@ def parse(docstring: Docstring, **options: Any) -> list[DocstringSection]: # no return _parsed_values_to_sections(parsed_values) -def _read_parameter(docstring: Docstring, offset: int, parsed_values: ParsedValues) -> int: +def _read_parameter( + docstring: Docstring, + offset: int, + parsed_values: ParsedValues, + *, + warn_unknown_params: bool = True, + **options: Any, # noqa: ARG001 +) -> int: parsed_directive = _parse_directive(docstring, offset) if parsed_directive.invalid: return parsed_directive.next_index @@ -135,6 +148,17 @@ def _read_parameter(docstring: Docstring, offset: int, parsed_values: ParsedValu _warn(docstring, 0, f"Duplicate parameter entry for '{name}'") return parsed_directive.next_index + if warn_unknown_params: + with suppress(AttributeError): # for parameters sections in objects without parameters + params = docstring.parent.parameters # type: ignore[union-attr] + if name not in params: + message = f"Parameter '{name}' does not appear in the function signature" + for starred_name in (f"*{name}", f"**{name}"): + if starred_name in params: + message += f". Did you mean '{starred_name}'?" + break + _warn(docstring, 0, message) + annotation = _determine_param_annotation(docstring, name, directive_type, parsed_values) default = _determine_param_default(docstring, name) @@ -187,7 +211,12 @@ def _determine_param_annotation( return annotation -def _read_parameter_type(docstring: Docstring, offset: int, parsed_values: ParsedValues) -> int: +def _read_parameter_type( + docstring: Docstring, + offset: int, + parsed_values: ParsedValues, + **options: Any, # noqa: ARG001 +) -> int: parsed_directive = _parse_directive(docstring, offset) if parsed_directive.invalid: return parsed_directive.next_index @@ -209,7 +238,12 @@ def _read_parameter_type(docstring: Docstring, offset: int, parsed_values: Parse return parsed_directive.next_index -def _read_attribute(docstring: Docstring, offset: int, parsed_values: ParsedValues) -> int: +def _read_attribute( + docstring: Docstring, + offset: int, + parsed_values: ParsedValues, + **options: Any, # noqa: ARG001 +) -> int: parsed_directive = _parse_directive(docstring, offset) if parsed_directive.invalid: return parsed_directive.next_index @@ -246,7 +280,12 @@ def _read_attribute(docstring: Docstring, offset: int, parsed_values: ParsedValu return parsed_directive.next_index -def _read_attribute_type(docstring: Docstring, offset: int, parsed_values: ParsedValues) -> int: +def _read_attribute_type( + docstring: Docstring, + offset: int, + parsed_values: ParsedValues, + **options: Any, # noqa: ARG001 +) -> int: parsed_directive = _parse_directive(docstring, offset) if parsed_directive.invalid: return parsed_directive.next_index @@ -268,7 +307,12 @@ def _read_attribute_type(docstring: Docstring, offset: int, parsed_values: Parse return parsed_directive.next_index -def _read_exception(docstring: Docstring, offset: int, parsed_values: ParsedValues) -> int: +def _read_exception( + docstring: Docstring, + offset: int, + parsed_values: ParsedValues, + **options: Any, # noqa: ARG001 +) -> int: parsed_directive = _parse_directive(docstring, offset) if parsed_directive.invalid: return parsed_directive.next_index @@ -282,7 +326,7 @@ def _read_exception(docstring: Docstring, offset: int, parsed_values: ParsedValu return parsed_directive.next_index -def _read_return(docstring: Docstring, offset: int, parsed_values: ParsedValues) -> int: +def _read_return(docstring: Docstring, offset: int, parsed_values: ParsedValues, **options: Any) -> int: # noqa: ARG001 parsed_directive = _parse_directive(docstring, offset) if parsed_directive.invalid: return parsed_directive.next_index @@ -307,7 +351,12 @@ def _read_return(docstring: Docstring, offset: int, parsed_values: ParsedValues) return parsed_directive.next_index -def _read_return_type(docstring: Docstring, offset: int, parsed_values: ParsedValues) -> int: +def _read_return_type( + docstring: Docstring, + offset: int, + parsed_values: ParsedValues, + **options: Any, # noqa: ARG001 +) -> int: parsed_directive = _parse_directive(docstring, offset) if parsed_directive.invalid: return parsed_directive.next_index diff --git a/tests/test_docstrings/test_sphinx.py b/tests/test_docstrings/test_sphinx.py index 9642bc6b..78c47047 100644 --- a/tests/test_docstrings/test_sphinx.py +++ b/tests/test_docstrings/test_sphinx.py @@ -491,6 +491,30 @@ def test_parse__param_type_twice_annotated__error_message(parse_sphinx: ParserTy assert "Duplicate parameter information for 'foo'" in warnings[0] +def test_warn_about_unknown_parameters(parse_sphinx: ParserType) -> None: + """Warn about unknown parameters in "Parameters" sections. + + Parameters: + parse_sphinx: Fixture parser. + """ + docstring = """ + + :param str a: {SOME_TEXT} + """ + + _, warnings = parse_sphinx( + docstring, + parent=Function( + "func", + parameters=Parameters( + Parameter("b"), + ), + ), + ) + assert len(warnings) == 1 + assert "Parameter 'a' does not appear in the function signature" in warnings[0] + + def test_parse__param_type_no_type__error_message(parse_sphinx: ParserType) -> None: """Parse a simple docstring.