Skip to content

Commit b8ac106

Browse files
committed
Add support for "Attributes" docstring section
1 parent 3d3f752 commit b8ac106

File tree

9 files changed

+151
-60
lines changed

9 files changed

+151
-60
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# (Fork) 0.0.7 (2024-06-22)
2+
3+
- Made "Attributes" a separate section from "Parameters" (for Google, Numpy, and Sphinx
4+
styles)
5+
16
# (Fork) 0.0.6 (2024-06-22)
27

38
- Merged in the latest changes from upstream (version 0.16)

docstring_parser/common.py

+35-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
import enum
33
import typing as T
44

5+
ATTR_KEYWORDS = {
6+
"attr",
7+
"attribute",
8+
}
59
PARAM_KEYWORDS = {
610
"param",
711
"parameter",
812
"arg",
913
"argument",
10-
"attribute",
1114
"key",
1215
"keyword",
1316
}
@@ -62,6 +65,9 @@ def __init__(
6265
self.args = args
6366
self.description = description
6467

68+
def __repr__(self) -> str:
69+
return str(self.__dict__)
70+
6571

6672
class DocstringParam(DocstringMeta):
6773
"""DocstringMeta symbolizing :param metadata."""
@@ -83,6 +89,26 @@ def __init__(
8389
self.default = default
8490

8591

92+
class DocstringAttr(DocstringMeta):
93+
"""DocstringMeta symbolizing :attr metadata."""
94+
95+
def __init__(
96+
self,
97+
args: T.List[str],
98+
description: T.Optional[str],
99+
arg_name: str,
100+
type_name: T.Optional[str],
101+
is_optional: T.Optional[bool],
102+
default: T.Optional[str],
103+
) -> None:
104+
"""Initialize self."""
105+
super().__init__(args, description)
106+
self.arg_name = arg_name
107+
self.type_name = type_name
108+
self.is_optional = is_optional
109+
self.default = default
110+
111+
86112
class DocstringReturns(DocstringMeta):
87113
"""DocstringMeta symbolizing :returns metadata."""
88114

@@ -179,6 +205,9 @@ def __init__(
179205
self.meta = [] # type: T.List[DocstringMeta]
180206
self.style = style # type: T.Optional[DocstringStyle]
181207

208+
def __repr__(self) -> str:
209+
return str(self.__dict__)
210+
182211
@property
183212
def description(self) -> T.Optional[str]:
184213
"""Return the full description of the function
@@ -198,6 +227,11 @@ def description(self) -> T.Optional[str]:
198227

199228
return "\n".join(ret)
200229

230+
@property
231+
def attrs(self) -> T.List[DocstringAttr]:
232+
"""Return a list of information on class attributes"""
233+
return [item for item in self.meta if isinstance(item, DocstringAttr)]
234+
201235
@property
202236
def params(self) -> T.List[DocstringParam]:
203237
"""Return a list of information on function params."""

docstring_parser/google.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
from enum import IntEnum
88

99
from .common import (
10+
ATTR_KEYWORDS,
1011
EXAMPLES_KEYWORDS,
1112
PARAM_KEYWORDS,
1213
RAISES_KEYWORDS,
1314
RETURNS_KEYWORDS,
1415
YIELDS_KEYWORDS,
1516
Docstring,
17+
DocstringAttr,
1618
DocstringExample,
1719
DocstringMeta,
1820
DocstringParam,
@@ -146,15 +148,15 @@ def _build_single_meta(section: Section, desc: str) -> DocstringMeta:
146148
return DocstringExample(
147149
args=[section.key], snippet=None, description=desc
148150
)
149-
if section.key in PARAM_KEYWORDS:
151+
if section.key in PARAM_KEYWORDS | ATTR_KEYWORDS:
150152
raise ParseError("Expected paramenter name.")
151153
return DocstringMeta(args=[section.key], description=desc)
152154

153155
@staticmethod
154156
def _build_multi_meta(
155157
section: Section, before: str, desc: str
156158
) -> DocstringMeta:
157-
if section.key in PARAM_KEYWORDS:
159+
if section.key in PARAM_KEYWORDS | ATTR_KEYWORDS:
158160
match = GOOGLE_TYPED_ARG_REGEX.match(before)
159161
if match:
160162
arg_name, type_name = match.group(1, 2)
@@ -173,7 +175,13 @@ def _build_multi_meta(
173175
match = GOOGLE_ARG_DESC_REGEX.match(desc)
174176
default = match.group(1) if match else None
175177

176-
return DocstringParam(
178+
DocstringSectionType = (
179+
DocstringParam
180+
if section.key in PARAM_KEYWORDS
181+
else DocstringAttr
182+
)
183+
184+
return DocstringSectionType(
177185
args=[section.key, before],
178186
description=desc,
179187
arg_name=arg_name,

docstring_parser/numpydoc.py

+28-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from .common import (
1313
Docstring,
14+
DocstringAttr,
1415
DocstringDeprecated,
1516
DocstringExample,
1617
DocstringMeta,
@@ -160,6 +161,31 @@ def _parse_item(self, key: str, value: str) -> DocstringParam:
160161
)
161162

162163

164+
class AttrSection(ParamSection):
165+
"""Parser for numpydoc attribute sections.
166+
167+
Note: the official Numpy docstring guide doesn't explicitly allow
168+
an "Attributes" section (it only states a "Parameters" section), but
169+
the Google style explicitly offers an "Attributes" section (see
170+
https://google.github.io/styleguide/pyguide.html#s3.8.4-comments-in-doc-strings).
171+
That's why we added this for Numpy style.
172+
"""
173+
174+
def _parse_item(self, key: str, value: str) -> DocstringAttr:
175+
docstringParam: DocstringParam = super()._parse_item(
176+
key=key,
177+
value=value,
178+
)
179+
return DocstringAttr(
180+
args=docstringParam.args,
181+
description=docstringParam.description,
182+
arg_name=docstringParam.arg_name,
183+
type_name=docstringParam.type_name,
184+
is_optional=docstringParam.is_optional,
185+
default=docstringParam.default,
186+
)
187+
188+
163189
class RaisesSection(_KVSection):
164190
"""Parser for numpydoc raises sections.
165191
@@ -296,8 +322,8 @@ def parse(self, text: str) -> T.Iterable[DocstringMeta]:
296322
RaisesSection("Raise", "raises"),
297323
RaisesSection("Warns", "warns"),
298324
RaisesSection("Warn", "warns"),
299-
ParamSection("Attributes", "attribute"),
300-
ParamSection("Attribute", "attribute"),
325+
AttrSection("Attributes", "attribute"),
326+
AttrSection("Attribute", "attribute"),
301327
ReturnsSection("Returns", "returns"),
302328
ReturnsSection("Return", "returns"),
303329
YieldsSection("Yields", "yields"),

docstring_parser/rest.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66

77
from .common import (
88
DEPRECATION_KEYWORDS,
9+
ATTR_KEYWORDS,
910
PARAM_KEYWORDS,
1011
RAISES_KEYWORDS,
1112
RETURNS_KEYWORDS,
1213
YIELDS_KEYWORDS,
1314
Docstring,
15+
DocstringAttr,
1416
DocstringDeprecated,
1517
DocstringMeta,
1618
DocstringParam,
@@ -26,7 +28,7 @@
2628
def _build_meta(args: T.List[str], desc: str) -> DocstringMeta:
2729
key = args[0]
2830

29-
if key in PARAM_KEYWORDS:
31+
if key in PARAM_KEYWORDS | ATTR_KEYWORDS:
3032
if len(args) == 3:
3133
key, type_name, arg_name = args
3234
if type_name.endswith("?"):
@@ -46,7 +48,11 @@ def _build_meta(args: T.List[str], desc: str) -> DocstringMeta:
4648
match = re.match(r".*defaults to (.+)", desc, flags=re.DOTALL)
4749
default = match.group(1).rstrip(".") if match else None
4850

49-
return DocstringParam(
51+
DocstringSectionType = (
52+
DocstringParam if key in PARAM_KEYWORDS else DocstringAttr
53+
)
54+
55+
return DocstringSectionType(
5056
args=args,
5157
description=desc,
5258
arg_name=arg_name,
@@ -176,7 +182,7 @@ def parse(text: str) -> Docstring:
176182
ret.meta.append(_build_meta(args, desc))
177183

178184
for meta in ret.meta:
179-
if isinstance(meta, DocstringParam):
185+
if isinstance(meta, (DocstringParam, DocstringAttr)):
180186
meta.type_name = meta.type_name or types.get(meta.arg_name)
181187
elif isinstance(meta, DocstringReturns):
182188
meta.type_name = meta.type_name or rtypes.get(meta.return_name)

docstring_parser/tests/test_google.py

+27-25
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ def test_params() -> None:
447447
def test_attributes() -> None:
448448
"""Test parsing attributes."""
449449
docstring = parse("Short description")
450-
assert len(docstring.params) == 0
450+
assert len(docstring.attrs) == 0
451451

452452
docstring = parse(
453453
"""
@@ -460,23 +460,24 @@ def test_attributes() -> None:
460460
ratio (Optional[float], optional): description 4
461461
"""
462462
)
463-
assert len(docstring.params) == 4
464-
assert docstring.params[0].arg_name == "name"
465-
assert docstring.params[0].type_name is None
466-
assert docstring.params[0].description == "description 1"
467-
assert not docstring.params[0].is_optional
468-
assert docstring.params[1].arg_name == "priority"
469-
assert docstring.params[1].type_name == "int"
470-
assert docstring.params[1].description == "description 2"
471-
assert not docstring.params[1].is_optional
472-
assert docstring.params[2].arg_name == "sender"
473-
assert docstring.params[2].type_name == "str"
474-
assert docstring.params[2].description == "description 3"
475-
assert docstring.params[2].is_optional
476-
assert docstring.params[3].arg_name == "ratio"
477-
assert docstring.params[3].type_name == "Optional[float]"
478-
assert docstring.params[3].description == "description 4"
479-
assert docstring.params[3].is_optional
463+
assert len(docstring.params) == 0 # they are under a new section "attrs"
464+
assert len(docstring.attrs) == 4
465+
assert docstring.attrs[0].arg_name == "name"
466+
assert docstring.attrs[0].type_name is None
467+
assert docstring.attrs[0].description == "description 1"
468+
assert not docstring.attrs[0].is_optional
469+
assert docstring.attrs[1].arg_name == "priority"
470+
assert docstring.attrs[1].type_name == "int"
471+
assert docstring.attrs[1].description == "description 2"
472+
assert not docstring.attrs[1].is_optional
473+
assert docstring.attrs[2].arg_name == "sender"
474+
assert docstring.attrs[2].type_name == "str"
475+
assert docstring.attrs[2].description == "description 3"
476+
assert docstring.attrs[2].is_optional
477+
assert docstring.attrs[3].arg_name == "ratio"
478+
assert docstring.attrs[3].type_name == "Optional[float]"
479+
assert docstring.attrs[3].description == "description 4"
480+
assert docstring.attrs[3].is_optional
480481

481482
docstring = parse(
482483
"""
@@ -488,15 +489,16 @@ def test_attributes() -> None:
488489
priority (int): description 2
489490
"""
490491
)
491-
assert len(docstring.params) == 2
492-
assert docstring.params[0].arg_name == "name"
493-
assert docstring.params[0].type_name is None
494-
assert docstring.params[0].description == (
492+
assert len(docstring.params) == 0
493+
assert len(docstring.attrs) == 2
494+
assert docstring.attrs[0].arg_name == "name"
495+
assert docstring.attrs[0].type_name is None
496+
assert docstring.attrs[0].description == (
495497
"description 1\nwith multi-line text"
496498
)
497-
assert docstring.params[1].arg_name == "priority"
498-
assert docstring.params[1].type_name == "int"
499-
assert docstring.params[1].description == "description 2"
499+
assert docstring.attrs[1].arg_name == "priority"
500+
assert docstring.attrs[1].type_name == "int"
501+
assert docstring.attrs[1].description == "description 2"
500502

501503

502504
def test_returns() -> None:

docstring_parser/tests/test_numpydoc.py

+27-25
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ def test_params() -> None:
415415
def test_attributes() -> None:
416416
"""Test parsing attributes."""
417417
docstring = parse("Short description")
418-
assert len(docstring.params) == 0
418+
assert len(docstring.attrs) == 0
419419

420420
docstring = parse(
421421
"""
@@ -433,23 +433,24 @@ def test_attributes() -> None:
433433
description 4
434434
"""
435435
)
436-
assert len(docstring.params) == 4
437-
assert docstring.params[0].arg_name == "name"
438-
assert docstring.params[0].type_name is None
439-
assert docstring.params[0].description == "description 1"
440-
assert not docstring.params[0].is_optional
441-
assert docstring.params[1].arg_name == "priority"
442-
assert docstring.params[1].type_name == "int"
443-
assert docstring.params[1].description == "description 2"
444-
assert not docstring.params[1].is_optional
445-
assert docstring.params[2].arg_name == "sender"
446-
assert docstring.params[2].type_name == "str"
447-
assert docstring.params[2].description == "description 3"
448-
assert docstring.params[2].is_optional
449-
assert docstring.params[3].arg_name == "ratio"
450-
assert docstring.params[3].type_name == "Optional[float]"
451-
assert docstring.params[3].description == "description 4"
452-
assert docstring.params[3].is_optional
436+
assert len(docstring.params) == 0
437+
assert len(docstring.attrs) == 4
438+
assert docstring.attrs[0].arg_name == "name"
439+
assert docstring.attrs[0].type_name is None
440+
assert docstring.attrs[0].description == "description 1"
441+
assert not docstring.attrs[0].is_optional
442+
assert docstring.attrs[1].arg_name == "priority"
443+
assert docstring.attrs[1].type_name == "int"
444+
assert docstring.attrs[1].description == "description 2"
445+
assert not docstring.attrs[1].is_optional
446+
assert docstring.attrs[2].arg_name == "sender"
447+
assert docstring.attrs[2].type_name == "str"
448+
assert docstring.attrs[2].description == "description 3"
449+
assert docstring.attrs[2].is_optional
450+
assert docstring.attrs[3].arg_name == "ratio"
451+
assert docstring.attrs[3].type_name == "Optional[float]"
452+
assert docstring.attrs[3].description == "description 4"
453+
assert docstring.attrs[3].is_optional
453454

454455
docstring = parse(
455456
"""
@@ -464,15 +465,16 @@ def test_attributes() -> None:
464465
description 2
465466
"""
466467
)
467-
assert len(docstring.params) == 2
468-
assert docstring.params[0].arg_name == "name"
469-
assert docstring.params[0].type_name is None
470-
assert docstring.params[0].description == (
468+
assert len(docstring.params) == 0
469+
assert len(docstring.attrs) == 2
470+
assert docstring.attrs[0].arg_name == "name"
471+
assert docstring.attrs[0].type_name is None
472+
assert docstring.attrs[0].description == (
471473
"description 1\nwith multi-line text"
472474
)
473-
assert docstring.params[1].arg_name == "priority"
474-
assert docstring.params[1].type_name == "int"
475-
assert docstring.params[1].description == "description 2"
475+
assert docstring.attrs[1].type_name == "int"
476+
assert docstring.attrs[1].arg_name == "priority"
477+
assert docstring.attrs[1].description == "description 2"
476478

477479

478480
def test_other_params() -> None:

0 commit comments

Comments
 (0)