Skip to content

Commit 4d0a9ee

Browse files
committed
feat: Support auto docstring style (infer from docstring)
1 parent 90978b4 commit 4d0a9ee

6 files changed

Lines changed: 289 additions & 99 deletions

File tree

docs/insiders/changelog.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
## Griffe Insiders
44

5+
[](){#insiders-1.3.1}
6+
### 1.3.1 <small>December 31, 2024</small> { id="1.3.1" }
7+
8+
- Accept per-style docstring options instead of generic options when docstring style is set to `auto`.
9+
In MkDocs, apply the following change:
10+
11+
```diff
12+
docstring_style: auto
13+
docstring_options:
14+
- ignore_init_summary: true
15+
+ per_style_options:
16+
+ google:
17+
+ ignore_init_summary: true
18+
```
19+
520
[](){#insiders-1.3.0}
621

722
### 1.3.0 <small>August 09, 2024</small> { id="1.3.0" }

docs/reference/api/docstrings/parsers.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424

2525
::: griffe.SphinxOptions
2626

27+
::: griffe.AutoOptions
28+
29+
::: griffe.PerStyleOptions
30+
2731
## **Advanced API**
2832

2933
::: griffe.Parser

mkdocs.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,10 @@ plugins:
229229
options:
230230
backlinks: tree
231231
docstring_options:
232-
ignore_init_summary: true
233-
docstring_style: google
232+
per_style_options:
233+
google:
234+
ignore_init_summary: true
235+
docstring_style: auto
234236
docstring_section_style: list
235237
extensions:
236238
- griffe_inherited_docstrings

src/griffe/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,13 @@
206206
ReturnChangedTypeBreakage,
207207
find_breaking_changes,
208208
)
209+
from griffe._internal.docstrings.auto import (
210+
AutoOptions,
211+
DocstringDetectionMethod,
212+
PerStyleOptions,
213+
infer_docstring_style,
214+
parse_auto,
215+
)
209216
from griffe._internal.docstrings.google import GoogleOptions, parse_google
210217
from griffe._internal.docstrings.models import (
211218
DocstringAdmonition,
@@ -245,12 +252,9 @@
245252
)
246253
from griffe._internal.docstrings.numpy import NumpyOptions, parse_numpy
247254
from griffe._internal.docstrings.parsers import (
248-
DocstringDetectionMethod,
249255
DocstringOptions,
250256
DocstringStyle,
251-
infer_docstring_style,
252257
parse,
253-
parse_auto,
254258
parsers,
255259
)
256260
from griffe._internal.docstrings.sphinx import SphinxOptions, parse_sphinx
@@ -409,6 +413,7 @@ def __getattr__(name: str) -> Any:
409413
"Attribute",
410414
"AttributeChangedTypeBreakage",
411415
"AttributeChangedValueBreakage",
416+
"AutoOptions",
412417
"Breakage",
413418
"BreakageKind",
414419
"BuiltinModuleError",
@@ -539,6 +544,7 @@ def __getattr__(name: str) -> Any:
539544
"Parameters",
540545
"ParametersType",
541546
"Parser",
547+
"PerStyleOptions",
542548
"ReturnChangedTypeBreakage",
543549
"RootNodeError",
544550
"SerializationMixin",
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
# This module defines functions to parse docstrings by guessing their style.
2+
3+
from __future__ import annotations
4+
5+
import re
6+
from typing import TYPE_CHECKING, Any, Literal, TypedDict
7+
from warnings import warn
8+
9+
from griffe._internal.enumerations import Parser
10+
11+
if TYPE_CHECKING:
12+
from griffe._internal.docstrings.google import GoogleOptions
13+
from griffe._internal.docstrings.models import DocstringSection
14+
from griffe._internal.docstrings.numpy import NumpyOptions
15+
from griffe._internal.docstrings.parsers import DocstringStyle
16+
from griffe._internal.docstrings.sphinx import SphinxOptions
17+
from griffe._internal.models import Docstring
18+
19+
20+
# This is not our preferred order, but the safest order for proper detection
21+
# using heuristics. Indeed, Google style sections sometimes appear in otherwise
22+
# plain markup docstrings, which could lead to false positives. Same for Numpy
23+
# sections, whose syntax is regular rST markup, and which can therefore appear
24+
# in plain markup docstrings too, even more often than Google sections.
25+
_default_style_order = [Parser.sphinx, Parser.google, Parser.numpy]
26+
27+
28+
DocstringDetectionMethod = Literal["heuristics", "max_sections"]
29+
"""The supported methods to infer docstring styles."""
30+
31+
32+
_patterns = {
33+
Parser.google: (
34+
r"\n[ \t]*{0}:([ \t]+.+)?\n[ \t]+.+",
35+
[
36+
"args",
37+
"arguments",
38+
"params",
39+
"parameters",
40+
"keyword args",
41+
"keyword arguments",
42+
"other args",
43+
"other arguments",
44+
"other params",
45+
"other parameters",
46+
"raises",
47+
"exceptions",
48+
"returns",
49+
"yields",
50+
"receives",
51+
"examples",
52+
"attributes",
53+
"functions",
54+
"methods",
55+
"classes",
56+
"modules",
57+
"warns",
58+
"warnings",
59+
],
60+
),
61+
Parser.numpy: (
62+
r"\n[ \t]*{0}\n[ \t]*---+\n",
63+
[
64+
"deprecated",
65+
"parameters",
66+
"other parameters",
67+
"returns",
68+
"yields",
69+
"receives",
70+
"raises",
71+
"warns",
72+
# "examples",
73+
"attributes",
74+
"functions",
75+
"methods",
76+
"classes",
77+
"modules",
78+
],
79+
),
80+
Parser.sphinx: (
81+
r"\n[ \t]*:{0}([ \t]+\w+)*:([ \t]+.+)?\n",
82+
[
83+
"param",
84+
"parameter",
85+
"arg",
86+
"argument",
87+
"key",
88+
"keyword",
89+
"type",
90+
"var",
91+
"ivar",
92+
"cvar",
93+
"vartype",
94+
"returns",
95+
"return",
96+
"rtype",
97+
"raises",
98+
"raise",
99+
"except",
100+
"exception",
101+
],
102+
),
103+
}
104+
105+
106+
class PerStyleOptions(TypedDict, total=False):
107+
"""Per-style options for docstring parsing."""
108+
109+
google: GoogleOptions
110+
"""Options for Google-style docstrings."""
111+
numpy: NumpyOptions
112+
"""Options for Numpy-style docstrings."""
113+
sphinx: SphinxOptions
114+
"""Options for Sphinx-style docstrings."""
115+
116+
117+
def infer_docstring_style(
118+
docstring: Docstring,
119+
*,
120+
method: DocstringDetectionMethod = "heuristics",
121+
style_order: list[Parser] | list[DocstringStyle] | None = None,
122+
default: Parser | DocstringStyle | None = None,
123+
per_style_options: PerStyleOptions | None = None,
124+
# YORE: Bump 2: Remove line.
125+
**options: Any,
126+
) -> tuple[Parser | None, list[DocstringSection] | None]:
127+
"""Infer the parser to use for the docstring.
128+
129+
[:octicons-heart-fill-24:{ .pulse } Sponsors only](../../../insiders/index.md){ .insiders } &mdash;
130+
[:octicons-tag-24: Insiders 1.3.0](../../../insiders/changelog.md#1.3.0).
131+
132+
The 'heuristics' method uses regular expressions. The 'max_sections' method
133+
parses the docstring with all parsers specified in `style_order` and returns
134+
the one who parsed the most sections.
135+
136+
If heuristics fail, the `default` parser is returned. If multiple parsers
137+
parsed the same number of sections, `style_order` is used to decide which
138+
one to return. The `default` parser is never used with the 'max_sections' method.
139+
140+
For non-Insiders versions, `default` is returned if specified, else the first
141+
parser in `style_order` is returned. If `style_order` is not specified,
142+
`None` is returned.
143+
144+
Additional options are parsed to the detected parser, if any.
145+
146+
Parameters:
147+
docstring: The docstring to parse.
148+
method: The method to use to infer the parser.
149+
style_order: The order of the styles to try when inferring the parser.
150+
default: The default parser to use if the inference fails.
151+
per_style_options: Additional parsing options per style.
152+
**options: Deprecated. Use `per_style_options` instead.
153+
154+
Returns:
155+
The inferred parser, and optionally parsed sections (when method is 'max_sections').
156+
"""
157+
from griffe._internal.docstrings.parsers import parsers # noqa: PLC0415
158+
159+
# YORE: Bump 2: Replace block with `per_style_options = per_style_options or {}`.
160+
if options:
161+
if per_style_options:
162+
raise ValueError("Cannot use both `options` and `per_style_options`.")
163+
warn("`**options` is deprecated. Use `per_style_options` instead.", DeprecationWarning, stacklevel=2)
164+
per_style_options = {"google": options, "numpy": options, "sphinx": options} # type: ignore[typeddict-item]
165+
elif not per_style_options:
166+
per_style_options = {}
167+
168+
style_order = [Parser(style) if isinstance(style, str) else style for style in style_order or _default_style_order]
169+
170+
if method == "heuristics":
171+
for style in style_order:
172+
pattern, replacements = _patterns[style]
173+
patterns = [
174+
re.compile(pattern.format(replacement), re.IGNORECASE | re.MULTILINE) for replacement in replacements
175+
]
176+
if any(pattern.search(docstring.value) for pattern in patterns):
177+
return style, None
178+
return default if default is None or isinstance(default, Parser) else Parser(default), None
179+
180+
if method == "max_sections":
181+
style_sections = {}
182+
for style in style_order:
183+
style_sections[style] = parsers[style](docstring, **per_style_options.get(style, {})) # type: ignore[arg-type]
184+
style_lengths = {style: len(section) for style, section in style_sections.items()}
185+
max_sections = max(style_lengths.values())
186+
for style in style_order:
187+
if style_lengths[style] == max_sections:
188+
return style, style_sections[style]
189+
190+
raise ValueError(f"Invalid method '{method}'.")
191+
192+
193+
class AutoOptions(TypedDict, total=False):
194+
"""Options for Auto-style docstrings."""
195+
196+
method: DocstringDetectionMethod
197+
"""The method to use to infer the parser."""
198+
style_order: list[Parser] | list[DocstringStyle] | None
199+
"""The order of styles to try when inferring the parser."""
200+
default: Parser | DocstringStyle | None
201+
"""The default parser to use if the inference fails."""
202+
per_style_options: PerStyleOptions | None
203+
"""Additional parsing options per style."""
204+
205+
206+
def parse_auto(
207+
docstring: Docstring,
208+
*,
209+
method: DocstringDetectionMethod = "heuristics",
210+
style_order: list[Parser] | list[DocstringStyle] | None = None,
211+
default: Parser | DocstringStyle | None = None,
212+
per_style_options: PerStyleOptions | None = None,
213+
# YORE: Bump 2: Remove line.
214+
**options: Any,
215+
) -> list[DocstringSection]:
216+
"""Parse a docstring by automatically detecting the style it uses.
217+
218+
[:octicons-heart-fill-24:{ .pulse } Sponsors only](../../../insiders/index.md){ .insiders } &mdash;
219+
[:octicons-tag-24: Insiders 1.3.0](../../../insiders/changelog.md#1.3.0).
220+
221+
See [`infer_docstring_style`][griffe.infer_docstring_style] for more information
222+
on the available parameters.
223+
224+
Parameters:
225+
docstring: The docstring to parse.
226+
method: The method to use to infer the parser.
227+
style_order: The order of the styles to try when inferring the parser.
228+
default: The default parser to use if the inference fails.
229+
per_style_options: Additional parsing options per style.
230+
**options: Deprecated. Use `per_style_options` instead.
231+
232+
Returns:
233+
A list of docstring sections.
234+
"""
235+
from griffe._internal.docstrings.parsers import parse # noqa: PLC0415
236+
237+
# YORE: Bump 2: Replace block with `per_style_options = per_style_options or {}`.
238+
if options:
239+
if per_style_options:
240+
raise ValueError("Cannot use both `options` and `per_style_options`.")
241+
warn("`**options` are deprecated. Use `per_style_options` instead.", DeprecationWarning, stacklevel=2)
242+
per_style_options = {"google": options, "numpy": options, "sphinx": options} # type: ignore[typeddict-item]
243+
elif not per_style_options:
244+
per_style_options = {}
245+
246+
style, sections = infer_docstring_style(
247+
docstring,
248+
method=method,
249+
style_order=style_order,
250+
default=default,
251+
per_style_options=per_style_options,
252+
)
253+
if sections is None:
254+
return parse(docstring, style, **per_style_options.get(style, {})) # type: ignore[arg-type,typeddict-item]
255+
return sections

0 commit comments

Comments
 (0)