Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/docstub/_docstrings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions src/docstub/_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand All @@ -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


Expand Down Expand Up @@ -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))

Expand Down
46 changes: 46 additions & 0 deletions tests/test_docstrings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading