Skip to content

Commit 8856704

Browse files
committed
Napoleon: Unify the type preprocessing logic
Previously, there were two type preprocessing functions: `_convert_type_spec` (used in Google-style docstrings) and `_convert_numpy_type_spec` (used in Numpy-style docstrings). The Google version simply applied type-alias translations or wrapped the text in a `:py:class:` role. The Numpy version does the same, plus adds special handling for keywords `optional` and `default` and delimiter words `or`, `of`, and `and`. This allows one to write in natural language, like `Array of int` instead of `Array[int]` or `Widget, optional` instead of `Optional[Widget]` or `Widget | None`. Numpy style is described in full at: https://numpydoc.readthedocs.io/en/latest/format.html#parameters This commit eliminates the distinction and allows Google-style docstrings to use these preprocessing rules.
1 parent 10f8548 commit 8856704

File tree

2 files changed

+35
-38
lines changed

2 files changed

+35
-38
lines changed

sphinx/ext/napoleon/docstring.py

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def postprocess(item: str) -> list[str]:
154154
return tokens
155155

156156

157-
def _token_type(token: str, location: str | None = None) -> str:
157+
def _token_type(token: str, debug_location: str | None = None) -> str:
158158
def is_numeric(token: str) -> bool:
159159
try:
160160
# use complex to make sure every numeric value is detected as literal
@@ -177,28 +177,28 @@ def is_numeric(token: str) -> bool:
177177
logger.warning(
178178
__('invalid value set (missing closing brace): %s'),
179179
token,
180-
location=location,
180+
location=debug_location,
181181
)
182182
type_ = 'literal'
183183
elif token.endswith('}'):
184184
logger.warning(
185185
__('invalid value set (missing opening brace): %s'),
186186
token,
187-
location=location,
187+
location=debug_location,
188188
)
189189
type_ = 'literal'
190190
elif token.startswith(("'", '"')):
191191
logger.warning(
192192
__('malformed string literal (missing closing quote): %s'),
193193
token,
194-
location=location,
194+
location=debug_location,
195195
)
196196
type_ = 'literal'
197197
elif token.endswith(("'", '"')):
198198
logger.warning(
199199
__('malformed string literal (missing opening quote): %s'),
200200
token,
201-
location=location,
201+
location=debug_location,
202202
)
203203
type_ = 'literal'
204204
elif token in {'optional', 'default'}:
@@ -213,10 +213,10 @@ def is_numeric(token: str) -> bool:
213213
return type_
214214

215215

216-
def _convert_numpy_type_spec(
216+
def _convert_type_spec(
217217
_type: str,
218-
location: str | None = None,
219218
translations: dict[str, str] | None = None,
219+
debug_location: str | None = None,
220220
) -> str:
221221
if translations is None:
222222
translations = {}
@@ -240,7 +240,7 @@ def convert_obj(
240240

241241
tokens = _tokenize_type_spec(_type)
242242
combined_tokens = _recombine_set_tokens(tokens)
243-
types = [(token, _token_type(token, location)) for token in combined_tokens]
243+
types = [(token, _token_type(token, debug_location)) for token in combined_tokens]
244244

245245
converters = {
246246
'literal': lambda x: '``%s``' % x,
@@ -258,15 +258,6 @@ def convert_obj(
258258
return converted
259259

260260

261-
def _convert_type_spec(_type: str, translations: dict[str, str] | None = None) -> str:
262-
"""Convert type specification to reference in reST."""
263-
if translations is not None and _type in translations:
264-
return translations[_type]
265-
if _type == 'None':
266-
return ':py:obj:`None`'
267-
return f':py:class:`{_type}`'
268-
269-
270261
class GoogleDocstring:
271262
"""Convert Google style docstrings to reStructuredText.
272263
@@ -433,6 +424,20 @@ def __str__(self) -> str:
433424
"""
434425
return '\n'.join(self.lines())
435426

427+
def _get_location(self) -> str | None:
428+
try:
429+
filepath = inspect.getfile(self._obj) if self._obj is not None else None
430+
except TypeError:
431+
filepath = None
432+
name = self._name
433+
434+
if filepath is None and name is None:
435+
return None
436+
elif filepath is None:
437+
filepath = ''
438+
439+
return f'{filepath}:docstring of {name}'
440+
436441
def lines(self) -> list[str]:
437442
"""Return the parsed lines of the docstring in reStructuredText format.
438443
@@ -490,7 +495,11 @@ def _consume_field(
490495
_type, _name = _name, _type
491496

492497
if _type and self._config.napoleon_preprocess_types:
493-
_type = _convert_type_spec(_type, self._config.napoleon_type_aliases or {})
498+
_type = _convert_type_spec(
499+
_type,
500+
translations=self._config.napoleon_type_aliases or {},
501+
debug_location=self._get_location(),
502+
)
494503

495504
indent = self._get_indent(line) + 1
496505
_descs = [_desc, *self._dedent(self._consume_indented_block(indent))]
@@ -538,7 +547,9 @@ def _consume_returns_section(
538547

539548
if _type and preprocess_types and self._config.napoleon_preprocess_types:
540549
_type = _convert_type_spec(
541-
_type, self._config.napoleon_type_aliases or {}
550+
_type,
551+
translations=self._config.napoleon_type_aliases or {},
552+
debug_location=self._get_location(),
542553
)
543554

544555
_desc = self.__class__(_desc, self._config).lines()
@@ -1202,20 +1213,6 @@ def __init__(
12021213
self._directive_sections = ['.. index::']
12031214
super().__init__(docstring, config, app, what, name, obj, options)
12041215

1205-
def _get_location(self) -> str | None:
1206-
try:
1207-
filepath = inspect.getfile(self._obj) if self._obj is not None else None
1208-
except TypeError:
1209-
filepath = None
1210-
name = self._name
1211-
1212-
if filepath is None and name is None:
1213-
return None
1214-
elif filepath is None:
1215-
filepath = ''
1216-
1217-
return f'{filepath}:docstring of {name}'
1218-
12191216
def _escape_args_and_kwargs(self, name: str) -> str:
12201217
func = super()._escape_args_and_kwargs
12211218

@@ -1242,10 +1239,10 @@ def _consume_field(
12421239
_type, _name = _name, _type
12431240

12441241
if self._config.napoleon_preprocess_types:
1245-
_type = _convert_numpy_type_spec(
1242+
_type = _convert_type_spec(
12461243
_type,
1247-
location=self._get_location(),
12481244
translations=self._config.napoleon_type_aliases or {},
1245+
debug_location=self._get_location(),
12491246
)
12501247

12511248
indent = self._get_indent(line) + 1

tests/test_extensions/test_ext_napoleon_docstring.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from sphinx.ext.napoleon.docstring import (
1616
GoogleDocstring,
1717
NumpyDocstring,
18-
_convert_numpy_type_spec,
18+
_convert_type_spec,
1919
_recombine_set_tokens,
2020
_token_type,
2121
_tokenize_type_spec,
@@ -1252,7 +1252,7 @@ def test_noindex(self):
12521252
.. method:: func(i, j)
12531253
:no-index:
12541254
1255-
1255+
12561256
description
12571257
""" # NoQA: W293
12581258
config = Config()
@@ -2675,7 +2675,7 @@ def test_convert_numpy_type_spec(self):
26752675
)
26762676

26772677
for spec, expected in zip(specs, converted, strict=True):
2678-
actual = _convert_numpy_type_spec(spec, translations=translations)
2678+
actual = _convert_type_spec(spec, translations=translations)
26792679
assert actual == expected
26802680

26812681
def test_parameter_types(self):

0 commit comments

Comments
 (0)