Skip to content
Closed
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
10 changes: 9 additions & 1 deletion docstring_parser/attrdoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,15 @@ def add_attribute_docstrings(
:param docstring: Docstring object where found attributes are added
:returns: list with names of added attributes
"""
params = set(p.arg_name for p in docstring.params)
# Dedup against *all* DocstringParam entries in meta (regardless of
# style-specific args[0] tag such as "param" vs "attribute"), not just
# docstring.params -- that property is intentionally narrower for
# NumpydocStyle docstrings (see Docstring.params) and would otherwise
# cause an attribute already documented via an explicit "Attributes"
# section to be re-added here, producing a duplicate entry.
params = set(
p.arg_name for p in docstring.meta if isinstance(p, DocstringParam)
)
for arg_name, (description, type_name, default) in (
AttributeDocstrings().get_attr_docs(obj).items()
):
Expand Down
22 changes: 20 additions & 2 deletions docstring_parser/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,26 @@ def description(self) -> T.Optional[str]:

@property
def params(self) -> T.List[DocstringParam]:
"""Return a list of information on function params."""
return [item for item in self.meta if isinstance(item, DocstringParam)]
"""Return a list of information on function params.

For NumpydocStyle docstrings, entries coming from an "Attributes"
section (which documents class attributes, not function parameters)
are excluded -- they are a semantically distinct concept tagged with
``args[0] == "attribute"`` by the numpydoc parser. Other styles keep
their existing behavior, since they don't share this ambiguity (e.g.
Google-style intentionally folds "Attributes" entries into
``params``, and this property is relied upon).
"""
return [
item
for item in self.meta
if isinstance(item, DocstringParam)
and not (
self.style == DocstringStyle.NUMPYDOC
and item.args
and item.args[0] == "attribute"
)
]

@property
def raises(self) -> T.List[DocstringRaises]:
Expand Down
20 changes: 13 additions & 7 deletions docstring_parser/numpydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,15 +475,20 @@ def process_sect(name: str, args: T.List[T.Any]):

process_sect(
"Parameters",
[item for item in docstring.params or [] if item.args[0] == "param"],
[
item
for item in docstring.meta
if isinstance(item, DocstringParam) and item.args[0] == "param"
],
)

process_sect(
"Attributes",
[
item
for item in docstring.params or []
if item.args[0] == "attribute"
for item in docstring.meta
if isinstance(item, DocstringParam)
and item.args[0] == "attribute"
],
)

Expand Down Expand Up @@ -511,17 +516,18 @@ def process_sect(name: str, args: T.List[T.Any]):
"Receives",
[
item
for item in docstring.params or []
if item.args[0] == "receives"
for item in docstring.meta
if isinstance(item, DocstringParam) and item.args[0] == "receives"
],
)

process_sect(
"Other Parameters",
[
item
for item in docstring.params or []
if item.args[0] == "other_param"
for item in docstring.meta
if isinstance(item, DocstringParam)
and item.args[0] == "other_param"
],
)

Expand Down
107 changes: 82 additions & 25 deletions docstring_parser/tests/test_numpydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,13 @@ def test_params() -> None:


def test_attributes() -> None:
"""Test parsing attributes."""
"""Test parsing attributes.

Note: ``Docstring.params`` intentionally excludes "Attributes" section
entries for numpydoc-style docstrings -- attributes document class
attributes, a concept distinct from function/constructor parameters. The
raw, parsed data is still available via ``Docstring.meta``.
"""
docstring = parse("Short description")
assert len(docstring.params) == 0

Expand All @@ -558,23 +564,27 @@ def test_attributes() -> None:
description 4
"""
)
assert len(docstring.params) == 4
assert docstring.params[0].arg_name == "name"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == "description 1"
assert not docstring.params[0].is_optional
assert docstring.params[1].arg_name == "priority"
assert docstring.params[1].type_name == "int"
assert docstring.params[1].description == "description 2"
assert not docstring.params[1].is_optional
assert docstring.params[2].arg_name == "sender"
assert docstring.params[2].type_name == "str"
assert docstring.params[2].description == "description 3"
assert docstring.params[2].is_optional
assert docstring.params[3].arg_name == "ratio"
assert docstring.params[3].type_name == "Optional[float]"
assert docstring.params[3].description == "description 4"
assert docstring.params[3].is_optional
# Attributes must NOT leak into the params convenience property.
assert len(docstring.params) == 0

attributes = [item for item in docstring.meta if item.args[0] == "attribute"]
assert len(attributes) == 4
assert attributes[0].arg_name == "name"
assert attributes[0].type_name is None
assert attributes[0].description == "description 1"
assert not attributes[0].is_optional
assert attributes[1].arg_name == "priority"
assert attributes[1].type_name == "int"
assert attributes[1].description == "description 2"
assert not attributes[1].is_optional
assert attributes[2].arg_name == "sender"
assert attributes[2].type_name == "str"
assert attributes[2].description == "description 3"
assert attributes[2].is_optional
assert attributes[3].arg_name == "ratio"
assert attributes[3].type_name == "Optional[float]"
assert attributes[3].description == "description 4"
assert attributes[3].is_optional

docstring = parse(
"""
Expand All @@ -589,15 +599,62 @@ def test_attributes() -> None:
description 2
"""
)
assert len(docstring.params) == 2
assert docstring.params[0].arg_name == "name"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == (
assert len(docstring.params) == 0

attributes = [item for item in docstring.meta if item.args[0] == "attribute"]
assert len(attributes) == 2
assert attributes[0].arg_name == "name"
assert attributes[0].type_name is None
assert attributes[0].description == (
"description 1\nwith multi-line text"
)
assert docstring.params[1].arg_name == "priority"
assert docstring.params[1].type_name == "int"
assert docstring.params[1].description == "description 2"
assert attributes[1].arg_name == "priority"
assert attributes[1].type_name == "int"
assert attributes[1].description == "description 2"


def test_params_excludes_attributes() -> None:
"""Regression test for issue #21.

``Docstring.params`` must not include entries from an "Attributes"
section, and when both "Parameters" and "Attributes" sections are
present, ``params`` must contain only the "Parameters" entries.
"""
# Reporter's reproducer: an Attributes-only docstring.
docstring = parse(
"""
Short description

Attributes
----------
data : int
Some attribute.
"""
)
assert docstring.params == []

# With both sections present, params must contain only the Parameters
# entries.
docstring = parse(
"""
Short description

Parameters
----------
x : int
An actual function parameter.

Attributes
----------
data : int
Some attribute.
"""
)
assert len(docstring.params) == 1
assert docstring.params[0].arg_name == "x"
assert [item.arg_name for item in docstring.meta if item.args[0] == "attribute"] == [
"data"
]


def test_other_params() -> None:
Expand Down
36 changes: 36 additions & 0 deletions docstring_parser/tests/test_parse_from_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from unittest.mock import patch

from docstring_parser import parse_from_object
from docstring_parser.common import DocstringParam

module_attr: int = 1
"""Description for module_attr"""
Expand Down Expand Up @@ -86,6 +87,41 @@ class WithoutSource:
assert len(docstring.params) == 0


def test_from_class_with_numpydoc_attributes_section_no_duplicate() -> None:
"""Regression test for issue #21 (attrdoc + NumpydocStyle interaction).

When a class uses NumpydocStyle and already documents an attribute in
an explicit "Attributes" section, ``add_attribute_docstrings`` must not
add a second, duplicate ``DocstringParam`` entry for the same attribute
just because ``Docstring.params`` (which now excludes "attribute"-tagged
numpydoc entries, see #21) no longer lists it.
"""

class WithNumpydocAttributes:
"""Short description.

Attributes
----------
attr_one : str
Description from the Attributes section.
"""

attr_one: str
"""Description from the source-level docstring"""

docstring = parse_from_object(WithNumpydocAttributes)

matches = [
item
for item in docstring.meta
if isinstance(item, DocstringParam) and item.arg_name == "attr_one"
]
assert len(matches) == 1
assert matches[0].description == (
"Description from the Attributes section."
)


def test_from_function() -> None:
"""Test the parse of a function docstring."""

Expand Down