Skip to content

Commit 4d2bd1e

Browse files
authored
Split autodoc name parsing utilities to a new module (#14033)
1 parent 6de23cd commit 4d2bd1e

File tree

6 files changed

+286
-231
lines changed

6 files changed

+286
-231
lines changed

sphinx/ext/autodoc/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
)
2626
from sphinx.ext.autodoc._event_listeners import between, cut_lines
2727
from sphinx.ext.autodoc._member_finder import ObjectMember, special_member_re
28+
from sphinx.ext.autodoc._names import py_ext_sig_re
2829
from sphinx.ext.autodoc._sentinels import ALL, EMPTY, SUPPRESS, UNINITIALIZED_ATTR
2930
from sphinx.ext.autodoc._sentinels import (
3031
INSTANCE_ATTR as INSTANCEATTR,
@@ -33,7 +34,6 @@
3334
SLOTS_ATTR as SLOTSATTR,
3435
)
3536
from sphinx.ext.autodoc.directive import AutodocDirective
36-
from sphinx.ext.autodoc.importer import py_ext_sig_re
3737
from sphinx.ext.autodoc.typehints import _merge_typehints
3838

3939
if TYPE_CHECKING:

sphinx/ext/autodoc/_names.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""Importer utilities for autodoc"""
2+
3+
from __future__ import annotations
4+
5+
import re
6+
from typing import TYPE_CHECKING
7+
8+
from sphinx.locale import __
9+
from sphinx.util import logging
10+
11+
if TYPE_CHECKING:
12+
from collections.abc import Mapping, Sequence
13+
14+
from sphinx.environment import _CurrentDocument
15+
from sphinx.ext.autodoc._property_types import _AutodocObjType
16+
17+
logger = logging.getLogger(__name__)
18+
19+
#: extended signature RE: with explicit module name separated by ::
20+
py_ext_sig_re = re.compile(
21+
r"""^ ([\w.]+::)? # explicit module name
22+
([\w.]+\.)? # module and/or class name(s)
23+
(\w+) \s* # thing name
24+
(?: \[\s*(.*?)\s*])? # optional: type parameters list
25+
(?: \((.*)\) # optional: arguments
26+
(?:\s* -> \s* (.*))? # return annotation
27+
)? $ # and nothing more
28+
""",
29+
re.VERBOSE,
30+
)
31+
32+
33+
def _parse_name(
34+
*,
35+
name: str,
36+
objtype: _AutodocObjType,
37+
current_document: _CurrentDocument,
38+
ref_context: Mapping[str, str | None],
39+
) -> tuple[str, tuple[str, ...], str | None, str | None] | None:
40+
"""Parse *name* into module name, path, arguments, and return annotation."""
41+
# Parse the definition in *name*.
42+
# autodoc directives for classes and functions can contain a signature,
43+
# which overrides the autogenerated one.
44+
matched = py_ext_sig_re.match(name)
45+
if matched is None:
46+
logger.warning(
47+
__('invalid signature for auto%s (%r)'),
48+
objtype,
49+
name,
50+
type='autodoc',
51+
)
52+
# need a module to import
53+
logger.warning(
54+
__(
55+
"don't know which module to import for autodocumenting "
56+
'%r (try placing a "module" or "currentmodule" directive '
57+
'in the document, or giving an explicit module name)'
58+
),
59+
name,
60+
type='autodoc',
61+
)
62+
return None
63+
64+
explicit_modname, path, base, _tp_list, args, retann = matched.groups()
65+
if args is not None:
66+
args = f'({args})'
67+
68+
# Support explicit module and class name separation via ``::``
69+
if explicit_modname is not None:
70+
module_name = explicit_modname.removesuffix('::')
71+
parents = path.rstrip('.').split('.') if path else ()
72+
else:
73+
module_name = None
74+
parents = ()
75+
76+
resolved = _resolve_name(
77+
objtype=objtype,
78+
module_name=module_name,
79+
path=path,
80+
base=base,
81+
parents=parents,
82+
current_document=current_document,
83+
ref_context_py_module=ref_context.get('py:module'),
84+
ref_context_py_class=ref_context.get('py:class', ''), # type: ignore[arg-type]
85+
)
86+
if resolved is None:
87+
return None
88+
module_name, parts = resolved
89+
90+
if objtype == 'module' and args:
91+
msg = __("signature arguments given for automodule: '%s'")
92+
logger.warning(msg, name, type='autodoc')
93+
return None
94+
if objtype == 'module' and retann:
95+
msg = __("return annotation given for automodule: '%s'")
96+
logger.warning(msg, name, type='autodoc')
97+
return None
98+
99+
if not module_name:
100+
# Could not resolve a module to import
101+
logger.warning(
102+
__(
103+
"don't know which module to import for autodocumenting "
104+
'%r (try placing a "module" or "currentmodule" directive '
105+
'in the document, or giving an explicit module name)'
106+
),
107+
name,
108+
type='autodoc',
109+
)
110+
return None
111+
112+
return module_name, parts, args, retann
113+
114+
115+
def _resolve_name(
116+
*,
117+
objtype: _AutodocObjType,
118+
module_name: str | None,
119+
path: str | None,
120+
base: str,
121+
parents: Sequence[str],
122+
current_document: _CurrentDocument,
123+
ref_context_py_module: str | None,
124+
ref_context_py_class: str,
125+
) -> tuple[str | None, tuple[str, ...]] | None:
126+
"""Resolve the module and name of the object to document given by the
127+
arguments and the current module/class.
128+
129+
Must return a pair of the module name and a chain of attributes; for
130+
example, it would return ``('zipfile', ('ZipFile', 'open'))`` for the
131+
``zipfile.ZipFile.open`` method.
132+
"""
133+
if objtype == 'module':
134+
if module_name is not None:
135+
logger.warning(
136+
__('"::" in automodule name doesn\'t make sense'), type='autodoc'
137+
)
138+
return (path or '') + base, ()
139+
140+
if objtype in {'class', 'exception', 'function', 'decorator', 'data', 'type'}:
141+
if module_name is not None:
142+
return module_name, (*parents, base)
143+
if path:
144+
module_name = path.rstrip('.')
145+
return module_name, (*parents, base)
146+
147+
# if documenting a toplevel object without explicit module,
148+
# it can be contained in another auto directive ...
149+
module_name = current_document.autodoc_module
150+
# ... or in the scope of a module directive
151+
if not module_name:
152+
module_name = ref_context_py_module
153+
# ... else, it stays None, which means invalid
154+
return module_name, (*parents, base)
155+
156+
if objtype in {'method', 'property', 'attribute'}:
157+
if module_name is not None:
158+
return module_name, (*parents, base)
159+
160+
if path:
161+
mod_cls = path.rstrip('.')
162+
else:
163+
# if documenting a class-level object without path,
164+
# there must be a current class, either from a parent
165+
# auto directive ...
166+
mod_cls = current_document.autodoc_class
167+
# ... or from a class directive
168+
if not mod_cls:
169+
mod_cls = ref_context_py_class
170+
# ... if still falsy, there's no way to know
171+
if not mod_cls:
172+
return None, ()
173+
module_name, _sep, cls = mod_cls.rpartition('.')
174+
parents = [cls]
175+
# if the module name is still missing, get it like above
176+
if not module_name:
177+
module_name = current_document.autodoc_module
178+
if not module_name:
179+
module_name = ref_context_py_module
180+
# ... else, it stays None, which means invalid
181+
return module_name, (*parents, base)
182+
183+
return None

sphinx/ext/autodoc/_signatures.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import TYPE_CHECKING, NewType, TypeVar
88

99
from sphinx.errors import PycodeError
10+
from sphinx.ext.autodoc._names import py_ext_sig_re
1011
from sphinx.ext.autodoc._property_types import _AssignStatementProperties
1112
from sphinx.ext.autodoc.preserve_defaults import update_default_value
1213
from sphinx.ext.autodoc.type_comment import _update_annotations_using_type_comments
@@ -336,8 +337,6 @@ def _extract_signatures_from_docstrings(
336337
props: _ItemProperties,
337338
tab_width: int,
338339
) -> list[_FormattedSignature]:
339-
from sphinx.ext.autodoc.importer import py_ext_sig_re
340-
341340
signatures: list[_FormattedSignature] = []
342341

343342
# candidates of the object name

0 commit comments

Comments
 (0)