"""Importer utilities for autodoc"""
from __future__ import annotations
import re
from typing import TYPE_CHECKING
from sphinx.ext.autodoc._shared import LOGGER
from sphinx.locale import __
if TYPE_CHECKING:
from collections.abc import Mapping, Sequence
from sphinx.environment import _CurrentDocument
from sphinx.ext.autodoc._property_types import _AutodocObjType
#: extended signature RE: with explicit module name separated by ::
py_ext_sig_re = re.compile(
r"""^ ([\w.]+::)? # explicit module name
([\w.]+\.)? # module and/or class name(s)
(\w+) \s* # thing name
(?: \[\s*(.*?)\s*])? # optional: type parameters list
(?: \((.*)\) # optional: arguments
(?:\s* -> \s* (.*))? # return annotation
)? $ # and nothing more
""",
re.VERBOSE,
)
def _parse_name(
*,
name: str,
objtype: _AutodocObjType,
current_document: _CurrentDocument,
ref_context: Mapping[str, str | None],
) -> tuple[str, tuple[str, ...], str | None, str | None] | None:
"""Parse *name* into module name, path, arguments, and return annotation."""
# Parse the definition in *name*.
# autodoc directives for classes and functions can contain a signature,
# which overrides the autogenerated one.
matched = py_ext_sig_re.match(name)
if matched is None:
LOGGER.warning(
__('invalid signature for auto%s (%r)'),
objtype,
name,
type='autodoc',
)
# need a module to import
LOGGER.warning(
__(
"don't know which module to import for autodocumenting "
'%r (try placing a "module" or "currentmodule" directive '
'in the document, or giving an explicit module name)'
),
name,
type='autodoc',
)
return None
explicit_modname, path, base, _tp_list, args, retann = matched.groups()
if args is not None:
args = f'({args})'
# Support explicit module and class name separation via ``::``
if explicit_modname is not None:
module_name = explicit_modname.removesuffix('::')
parents = path.rstrip('.').split('.') if path else ()
else:
module_name = None
parents = ()
resolved = _resolve_name(
objtype=objtype,
module_name=module_name,
path=path,
base=base,
parents=parents,
current_document=current_document,
ref_context_py_module=ref_context.get('py:module'),
ref_context_py_class=ref_context.get('py:class', ''), # type: ignore[arg-type]
)
if resolved is None:
return None
module_name, parts = resolved
if objtype == 'module' and args:
msg = __("signature arguments given for automodule: '%s'")
LOGGER.warning(msg, name, type='autodoc')
return None
if objtype == 'module' and retann:
msg = __("return annotation given for automodule: '%s'")
LOGGER.warning(msg, name, type='autodoc')
return None
if not module_name:
# Could not resolve a module to import
LOGGER.warning(
__(
"don't know which module to import for autodocumenting "
'%r (try placing a "module" or "currentmodule" directive '
'in the document, or giving an explicit module name)'
),
name,
type='autodoc',
)
return None
return module_name, parts, args, retann
def _resolve_name(
*,
objtype: _AutodocObjType,
module_name: str | None,
path: str | None,
base: str,
parents: Sequence[str],
current_document: _CurrentDocument,
ref_context_py_module: str | None,
ref_context_py_class: str,
) -> tuple[str | None, tuple[str, ...]] | None:
"""Resolve the module and name of the object to document given by the
arguments and the current module/class.
Must return a pair of the module name and a chain of attributes; for
example, it would return ``('zipfile', ('ZipFile', 'open'))`` for the
``zipfile.ZipFile.open`` method.
"""
if objtype == 'module':
if module_name is not None:
LOGGER.warning(
__('"::" in automodule name doesn\'t make sense'), type='autodoc'
)
return (path or '') + base, ()
if objtype in {'class', 'exception', 'function', 'decorator', 'data', 'type'}:
if module_name is not None:
return module_name, (*parents, base)
if path:
module_name = path.rstrip('.')
return module_name, (*parents, base)
# if documenting a toplevel object without explicit module,
# it can be contained in another auto directive ...
module_name = current_document.autodoc_module
# ... or in the scope of a module directive
if not module_name:
module_name = ref_context_py_module
# ... else, it stays None, which means invalid
return module_name, (*parents, base)
if objtype in {'method', 'property', 'attribute'}:
if module_name is not None:
return module_name, (*parents, base)
if path:
mod_cls = path.rstrip('.')
else:
# if documenting a class-level object without path,
# there must be a current class, either from a parent
# auto directive ...
mod_cls = current_document.autodoc_class
# ... or from a class directive
if not mod_cls:
mod_cls = ref_context_py_class
# ... if still falsy, there's no way to know
if not mod_cls:
return None, ()
module_name, _sep, cls = mod_cls.rpartition('.')
parents = [cls]
# if the module name is still missing, get it like above
if not module_name:
module_name = current_document.autodoc_module
if not module_name:
module_name = ref_context_py_module
# ... else, it stays None, which means invalid
return module_name, (*parents, base)
return None