Skip to content

Commit f97e19f

Browse files
Fix: Napoleon NumpyDocstring incorrect section ordering (Closes #13180)
Implements section reordering in NumpyDocstring._parse to ensure 'Attributes' and 'Methods' sections appear after 'Parameters', matching numpydoc standard logic. This is achieved by collecting sections during parsing then reordering them before generating the final parsed lines.
1 parent 038fc40 commit f97e19f

File tree

1 file changed

+60
-5
lines changed

1 file changed

+60
-5
lines changed

sphinx/ext/napoleon/docstring.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,18 +225,15 @@ def _convert_type_spec(
225225
combined_tokens = _recombine_set_tokens(tokens)
226226
types = [(token, _token_type(token, debug_location)) for token in combined_tokens]
227227

228-
converters = {
228+
converters: dict[str, Callable[[str], str]] = {
229229
'literal': lambda x: f'``{x}``',
230230
'obj': lambda x: _convert_type_spec_obj(x, translations),
231231
'control': lambda x: f'*{x}*',
232232
'delimiter': lambda x: x,
233233
'reference': lambda x: x,
234234
}
235235

236-
converted = ''.join(
237-
converters.get(type_)(token) # type: ignore[misc]
238-
for token, type_ in types
239-
)
236+
converted = ''.join(converters[type_](token) for token, type_ in types)
240237

241238
return converted
242239

@@ -1220,6 +1217,64 @@ def __init__(
12201217
self._directive_sections = ['.. index::']
12211218
super().__init__(docstring, config, app, what, name, obj, options)
12221219

1220+
def _parse(self) -> None:
1221+
self._parsed_lines = self._consume_empty()
1222+
1223+
if self._name and self._what in {'attribute', 'data', 'property'}:
1224+
res: list[str] = []
1225+
with contextlib.suppress(StopIteration):
1226+
res = self._parse_attribute_docstring()
1227+
1228+
self._parsed_lines.extend(res)
1229+
return
1230+
1231+
sections: list[tuple[str, list[str]]] = []
1232+
1233+
while self._lines:
1234+
if self._is_section_header():
1235+
try:
1236+
section = self._consume_section_header()
1237+
self._is_in_section = True
1238+
self._section_indent = self._get_current_indent()
1239+
if _directive_regex.match(section):
1240+
lines = [section, *self._consume_to_next_section()]
1241+
sections.append((section, lines))
1242+
else:
1243+
lines = self._sections[section.lower()](section)
1244+
sections.append((section.lower(), lines))
1245+
finally:
1246+
self._is_in_section = False
1247+
self._section_indent = 0
1248+
else:
1249+
if not self._parsed_lines and not sections:
1250+
lines = self._consume_contiguous() + self._consume_empty()
1251+
self._parsed_lines.extend(lines)
1252+
else:
1253+
lines = self._consume_to_next_section()
1254+
sections.append(('_msg', lines))
1255+
1256+
# Reorder sections: Attributes and Methods should come after Parameters
1257+
excluded_sections = {'attributes', 'methods'}
1258+
attributes_secs = [s for s in sections if s[0] == 'attributes']
1259+
methods_secs = [s for s in sections if s[0] == 'methods']
1260+
other_secs = [s for s in sections if s[0] not in excluded_sections]
1261+
1262+
insert_idx = 0
1263+
for i, (name, _section_lines) in enumerate(other_secs):
1264+
if name == 'parameters':
1265+
insert_idx = i + 1
1266+
break
1267+
1268+
final_sections = (
1269+
other_secs[:insert_idx]
1270+
+ attributes_secs
1271+
+ methods_secs
1272+
+ other_secs[insert_idx:]
1273+
)
1274+
1275+
for _name, lines in final_sections:
1276+
self._parsed_lines.extend(lines)
1277+
12231278
def _escape_args_and_kwargs(self, name: str) -> str:
12241279
func = super()._escape_args_and_kwargs
12251280

0 commit comments

Comments
 (0)