diff --git a/src/docstub/_docstrings.py b/src/docstub/_docstrings.py index a57badb..f4265c7 100644 --- a/src/docstub/_docstrings.py +++ b/src/docstub/_docstrings.py @@ -271,7 +271,7 @@ def doctype_to_annotation( details = error.get_context(doctype) details = details.replace("^", click.style("^", fg="red", bold=True)) stats.inc_counter("doctype_syntax_errors") - reporter.error("Invalid syntax in docstring type annotation", details=details) + reporter.error("Invalid syntax in doctype", details=details) return FallbackAnnotation except lark.visitors.VisitError as error: @@ -516,7 +516,7 @@ def _section_annotations(self, name): if param.name in annotated_params: self.reporter.warn( - "Duplicate parameter / attribute name in docstring", + f"Duplicate name in docstring section '{name}'", details=self.reporter.underline(param.name), ) continue diff --git a/src/docstub/_report.py b/src/docstub/_report.py index 773a1bd..5ac82ff 100644 --- a/src/docstub/_report.py +++ b/src/docstub/_report.py @@ -199,7 +199,7 @@ def __post_init__(self): @staticmethod def underline(line, *, char="^"): - """Underline `line` with the given `char`. + r"""Underline `line` with the given `char`. Parameters ---------- @@ -209,9 +209,20 @@ def underline(line, *, char="^"): Returns ------- underlined : str + + Examples + -------- + >>> ContextReporter.underline("foo") + 'foo\n\x1b[31m\x1b[1m^^^\x1b[0m' + >>> ContextReporter.underline("foo", char="x") + 'foo\n\x1b[31m\x1b[1mxxx\x1b[0m' + >>> ContextReporter.underline("") + '\n\x1b[31m\x1b[1m^\x1b[0m' """ assert len(char) == 1 - underlined = f"{line}\n{click.style(char * len(line), fg='red', bold=True)}" + # Underline of at least one char, so that it can indicate an empty line + underline = char * max(len(line), 1) + underlined = f"{line}\n{click.style(underline, fg='red', bold=True)}" return underlined @@ -340,6 +351,7 @@ def emit_grouped(self): groups[group_id] = groups.get(group_id, []) groups[group_id].append(record) + # TODO Presort by level and message too? # Show largest groups last groups_by_size = sorted(groups.values(), key=lambda x: len(x)) diff --git a/tests/test_docstrings.py b/tests/test_docstrings.py index 078aaa5..8458028 100644 --- a/tests/test_docstrings.py +++ b/tests/test_docstrings.py @@ -302,3 +302,49 @@ def test_unknown_section_logged(self, caplog): assert caplog.messages == ["Warning in NumPyDoc while parsing docstring"] assert caplog.records[0].details == "Unknown section To Do" + + def test_missing_section_title_1(self, caplog): + docstring = dedent( + """First line. + + Returns + ------- + bool + Some number. + + NumPyDoc parses this paragraph as another parameter (or it tries to). + A new section title should precede this to avoid that. + """ + ) + annotations = DocstringAnnotations(docstring) + assert annotations.returns + assert annotations.returns.value == "bool" + assert caplog.messages == ["Duplicate name in docstring section 'Returns'"] * 2 + assert caplog.records[0].details == "\n\x1b[31m\x1b[1m^\x1b[0m" + assert caplog.records[1].details == "\n\x1b[31m\x1b[1m^\x1b[0m" + + def test_missing_section_title_2(self, caplog): + docstring = dedent( + """First line. + + Returns + ------- + out : bool + Some number. + + NumPyDoc parses this paragraph as another parameter (or it tries to). + A new section title should precede this to avoid that. + """ + ) + annotations = DocstringAnnotations(docstring) + assert annotations.returns + assert annotations.returns.value == "tuple[bool, Incomplete]" + assert caplog.messages == [ + "Invalid syntax in doctype", + "Duplicate name in docstring section 'Returns'", + ] + assert caplog.records[0].details == ( + "NumPyDoc parses this paragraph as another paramet\n" + " \x1b[31m\x1b[1m^\x1b[0m\n" + ) + assert caplog.records[1].details == "\n\x1b[31m\x1b[1m^\x1b[0m"