diff --git a/CHANGELOG.md b/CHANGELOG.md index f346d807..7e127f5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - General: **Breaking:** Remove `DocstringMeta.args`, which each style filled differently; the new `key`, `name` and `kind` fields say the same, and the meta classes no longer take `args` - General: **Breaking:** Read the keyword sets in `common`, such as `PARAM_KEYWORDS`, once on import; pass `sections` instead of changing them - General: **Breaking:** Make `DocstringStyle` values the style names, such as `"google"`, instead of numbers +- General: **Breaking:** Leave attributes out of `Docstring.params`, and list them in the new `Docstring.attrs` - Google, Numpydoc: **Breaking:** Remove customising parsing by overriding internals such as `titles_re`, `title_pattern`, `_build_meta` and `KV_REGEX`; use `SectionSpec` instead - General: Parse docstrings with `\r\n` or `\r` line endings the same as with `\n`, on every Python version - General: Dedent docstrings the same way on every Python version; from 3.13, `inspect.cleandoc` dedented nothing after a blank line holding a tab or form feed @@ -10,6 +11,7 @@ - General: Quote at most the first 60 characters of the text a `ParseError` points at, instead of the whole chunk - General: Parse a docstring with no fields or sections once in auto-detection instead of four times, making it about 47% faster - General: Raise `ValueError` instead of `KeyError` from `compose()` when the docstring has no style and none is given +- General: Stop `combine_docstrings` from taking an attribute named like a param as that param - General: Give every meta a `kind`, a `Kind` that tells what it documents, such as a keyword param or a warning - General: Compose items into sections by their `kind`, so a meta made by hand needs `kind=` for a sub-kind such as `Kind.ATTRIBUTE` - General: Add `docstring_parser.sections`, where a `SectionSpec` declares a section or field for any style; pass a list as `sections` to `parse()` or a parser, starting from `default_sections(style)` diff --git a/docstring_parser/attrdoc.py b/docstring_parser/attrdoc.py index 9f8ec3af..125bbd24 100644 --- a/docstring_parser/attrdoc.py +++ b/docstring_parser/attrdoc.py @@ -110,7 +110,7 @@ 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) + params = set(p.arg_name for p in [*docstring.params, *docstring.attrs]) for arg_name, (description, type_name, default) in ( AttributeDocstrings().get_attr_docs(obj).items() ): diff --git a/docstring_parser/common.py b/docstring_parser/common.py index 34213b52..8eb98b65 100644 --- a/docstring_parser/common.py +++ b/docstring_parser/common.py @@ -306,8 +306,23 @@ 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, without the + attributes.""" + return [ + item + for item in self.meta + if isinstance(item, DocstringParam) + and item.kind is not Kind.ATTRIBUTE + ] + + @property + def attrs(self) -> T.List[DocstringParam]: + """Return a list of information on attributes.""" + return [ + item + for item in self.meta + if isinstance(item, DocstringParam) and item.kind is Kind.ATTRIBUTE + ] @property def raises(self) -> T.List[DocstringRaises]: diff --git a/docstring_parser/tests/test_kinds.py b/docstring_parser/tests/test_kinds.py index 42a5dde6..926c49e0 100644 --- a/docstring_parser/tests/test_kinds.py +++ b/docstring_parser/tests/test_kinds.py @@ -94,3 +94,27 @@ def test_kind_must_fit_class() -> None: def test_sub_kinds(style: DocstringStyle, text: str, kinds: list) -> None: """Test that each style tells the kinds of params and raises apart.""" assert [m.kind for m in parse(text, style).meta] == kinds + + +@pytest.mark.parametrize( + ("style", "text"), + [ + (DocstringStyle.REST, ":param x: X.\n:attribute y: Y."), + (DocstringStyle.EPYDOC, "@param x: X.\n@ivar y: Y."), + ( + DocstringStyle.GOOGLE, + "S.\n\nArgs:\n x: X.\n\nAttributes:\n y: Y.", + ), + ( + DocstringStyle.NUMPYDOC, + "S.\n\nParameters\n----------\nx\n X.\n\n" + "Attributes\n----------\ny\n Y.", + ), + ], + ids=lambda v: v.name.lower() if isinstance(v, DocstringStyle) else "", +) +def test_params_leave_out_attributes(style: DocstringStyle, text: str) -> None: + """Test that params and attrs each list their own items.""" + docstring = parse(text, style) + assert [p.arg_name for p in docstring.params] == ["x"] + assert [a.arg_name for a in docstring.attrs] == ["y"] diff --git a/docstring_parser/tests/test_parse_from_object.py b/docstring_parser/tests/test_parse_from_object.py index a2f24acf..fd46b98b 100644 --- a/docstring_parser/tests/test_parse_from_object.py +++ b/docstring_parser/tests/test_parse_from_object.py @@ -16,10 +16,11 @@ def test_from_module_attribute_docstrings() -> None: assert docstring.short_description is not None assert "parse_from_object" in docstring.short_description - assert len(docstring.params) == 1 - assert docstring.params[0].arg_name == "module_attr" - assert docstring.params[0].type_name == "int" - assert docstring.params[0].description == "Description for module_attr" + assert len(docstring.attrs) == 1 + assert docstring.params == [] + assert docstring.attrs[0].arg_name == "module_attr" + assert docstring.attrs[0].type_name == "int" + assert docstring.attrs[0].description == "Description for module_attr" def test_from_class_attribute_docstrings() -> None: @@ -40,13 +41,14 @@ class StandardCase: assert docstring.short_description == "Short description" assert docstring.long_description == "Long description" assert docstring.description == "Short description\nLong description" - assert len(docstring.params) == 2 - assert docstring.params[0].arg_name == "attr_one" - assert docstring.params[0].type_name == "str" - assert docstring.params[0].description == "Description for attr_one" - assert docstring.params[1].arg_name == "attr_two" - assert docstring.params[1].type_name == "bool" - assert docstring.params[1].description == "Description for attr_two" + assert len(docstring.attrs) == 2 + assert docstring.params == [] + assert docstring.attrs[0].arg_name == "attr_one" + assert docstring.attrs[0].type_name == "str" + assert docstring.attrs[0].description == "Description for attr_one" + assert docstring.attrs[1].arg_name == "attr_two" + assert docstring.attrs[1].type_name == "bool" + assert docstring.attrs[1].description == "Description for attr_two" def test_from_class_attribute_docstrings_without_type() -> None: @@ -61,10 +63,11 @@ class WithoutType: assert docstring.short_description is None assert docstring.long_description is None assert docstring.description is None - assert len(docstring.params) == 1 - assert docstring.params[0].arg_name == "attr_one" - assert docstring.params[0].type_name is None - assert docstring.params[0].description == "Description for attr_one" + assert len(docstring.attrs) == 1 + assert docstring.params == [] + assert docstring.attrs[0].arg_name == "attr_one" + assert docstring.attrs[0].type_name is None + assert docstring.attrs[0].description == "Description for attr_one" def test_from_class_without_source() -> None: @@ -85,6 +88,7 @@ class WithoutSource: assert docstring.long_description is None assert docstring.description == "Short description" assert len(docstring.params) == 0 + assert len(docstring.attrs) == 0 def test_from_function() -> None: