Combine name resolution and importing (#13797)

This commit is contained in:
Adam Turner authored and GitHub committed 2025-08-02 01:01:21 +01:00
1 parent 556a5f1095
commit 4bed65e18f
3 files changed
+571 -640

No files matched your search

+67 -376
View File
@@ -5,7 +5,6 @@ import operator
import re
import sys
from inspect import Parameter, Signature
from pathlib import Path
from typing import TYPE_CHECKING, NewType, TypeVar
from docutils.statemachine import StringList
@@ -21,13 +20,6 @@ from sphinx.ext.autodoc._directive_options import (
member_order_option,
members_option,
)
from sphinx.ext.autodoc._property_types import (
_AssignStatementProperties,
_ClassDefProperties,
_FunctionDefProperties,
_ItemProperties,
_ModuleProperties,
)
from sphinx.ext.autodoc._sentinels import (
ALL,
INSTANCE_ATTR,
@@ -38,16 +30,12 @@ from sphinx.ext.autodoc._sentinels import (
)
from sphinx.ext.autodoc.importer import (
_get_attribute_comment,
_import_assignment_attribute,
_import_assignment_data,
_import_class,
_import_method,
_import_object,
_import_property,
_is_runtime_instance_attribute_not_commented,
_load_object_by_name,
_resolve_name,
get_class_members,
)
from sphinx.ext.autodoc.mock import ismock, mock, undecorate
from sphinx.ext.autodoc.mock import ismock, undecorate
from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer
from sphinx.util import inspect, logging
@@ -70,7 +58,13 @@ if TYPE_CHECKING:
from sphinx.environment import BuildEnvironment, _CurrentDocument
from sphinx.events import EventManager
from sphinx.ext.autodoc._directive_options import _AutoDocumenterOptions
from sphinx.ext.autodoc._property_types import _AutodocFuncProperty
from sphinx.ext.autodoc._property_types import (
_AssignStatementProperties,
_ClassDefProperties,
_FunctionDefProperties,
_ItemProperties,
_ModuleProperties,
)
from sphinx.ext.autodoc.directive import DocumenterBridge
from sphinx.registry import SphinxComponentRegistry
from sphinx.util.typing import OptionSpec, _RestifyMode
@@ -230,6 +224,8 @@ class Documenter:
# the module analyzer to get at attribute docs, or None
self.analyzer: ModuleAnalyzer | None = None
self._load_object_has_been_called = False
@property
def documenters(self) -> dict[str, type[Documenter]]:
"""Returns registered Documenter classes"""
@@ -242,6 +238,44 @@ class Documenter:
else:
self.directive.result.append('', source, *lineno)
def _load_object_by_name(self) -> Literal[True] | None:
if self._load_object_has_been_called:
return True
ret = _load_object_by_name(
name=self.name,
objtype=self.objtype,
mock_imports=self.config.autodoc_mock_imports,
type_aliases=self.config.autodoc_type_aliases,
current_document=self._current_document,
env=self.env,
get_attr=self.get_attr,
)
if ret is None:
return None
props, args, retann, module, parent = ret
self.props = props
self.args = args
self.retann = retann
self.modname = props.module_name
self.objpath = list(props.parts)
self.fullname = props.full_name
self.module = module
self.parent = parent
self.object_name = props.name
self.object = props._obj
if self.objtype == 'method':
if 'staticmethod' in props.properties: # type: ignore[attr-defined]
# document static members before regular methods
self.member_order -= 1 # type: ignore[misc]
elif 'classmethod' in props.properties: # type: ignore[attr-defined]
# document class methods before static methods as
# they usually behave as alternative constructors
self.member_order -= 2 # type: ignore[misc]
self._load_object_has_been_called = True
return True
def resolve_name(
self, modname: str | None, parents: Any, path: str, base: str
) -> tuple[str | None, list[str]]:
@@ -252,71 +286,19 @@ class Documenter:
example, it would return ``('zipfile', ['ZipFile', 'open'])`` for the
``zipfile.ZipFile.open`` method.
"""
if isinstance(self, ModuleDocumenter):
if modname is not None:
logger.warning(
__('"::" in automodule name doesn\'t make sense'), type='autodoc'
)
return (path or '') + base, []
if isinstance(
self,
(
ModuleLevelDocumenter,
FunctionDocumenter,
ClassDocumenter,
DataDocumenter,
),
):
if modname is not None:
return modname, [*parents, base]
if path:
modname = path.rstrip('.')
return modname, [*parents, base]
# if documenting a toplevel object without explicit module,
# it can be contained in another auto directive ...
modname = self._current_document.autodoc_module
# ... or in the scope of a module directive
if not modname:
modname = self.env.ref_context.get('py:module')
# ... else, it stays None, which means invalid
return modname, [*parents, base]
if isinstance(
self,
(
ClassLevelDocumenter,
MethodDocumenter,
AttributeDocumenter,
PropertyDocumenter,
),
):
if modname is not None:
return modname, [*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 = self._current_document.autodoc_class
# ... or from a class directive
if not mod_cls:
mod_cls = self.env.ref_context.get('py:class', '')
# ... if still falsy, there's no way to know
if not mod_cls:
return None, []
modname, _sep, cls = mod_cls.rpartition('.')
parents = [cls]
# if the module name is still missing, get it like above
if not modname:
modname = self._current_document.autodoc_module
if not modname:
modname = self.env.ref_context.get('py:module')
# ... else, it stays None, which means invalid
return modname, [*parents, base]
ret = _resolve_name(
objtype=self.objtype,
module_name=modname,
path=path,
base=base,
parents=parents,
current_document=self._current_document,
ref_context_py_module=self.env.ref_context.get('py:module'),
ref_context_py_class=self.env.ref_context.get('py:class', ''),
)
if ret is not None:
module_name, parts = ret
return module_name, list(parts)
msg = 'must be implemented in subclasses'
raise NotImplementedError(msg)
@@ -327,39 +309,7 @@ class Documenter:
Returns True and sets *self.modname*, *self.objpath*, *self.fullname*,
*self.args* and *self.retann* if parsing and resolving was successful.
"""
# first, parse the definition -- auto directives for classes and
# functions can contain a signature which is then used instead of
# an autogenerated one
matched = py_ext_sig_re.match(self.name)
if matched is None:
logger.warning(
__('invalid signature for auto%s (%r)'),
self.objtype,
self.name,
type='autodoc',
)
return False
explicit_modname, path, base, _tp_list, args, retann = matched.groups()
# support explicit module and class name separation via ::
if explicit_modname is not None:
modname = explicit_modname[:-2]
parents = path.rstrip('.').split('.') if path else []
else:
modname = None
parents = []
with mock(self.config.autodoc_mock_imports):
modname, self.objpath = self.resolve_name(modname, parents, path, base)
if not modname:
return False
self.modname = modname
self.args = args
self.retann = retann
self.fullname = '.'.join((self.modname or '', *self.objpath))
return True
return self._load_object_by_name() is not None
def import_object(self, raiseerror: bool = False) -> bool:
"""Import the object given by *self.modname* and *self.objpath* and set
@@ -367,66 +317,7 @@ class Documenter:
Returns True if successful, False if an error occurred.
"""
try:
im = _import_object(
module_name=self.modname,
obj_path=self.objpath,
mock_imports=self.config.autodoc_mock_imports,
get_attr=self.get_attr,
)
except ImportError as exc:
if raiseerror:
raise
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False
self.object = obj = im.__dict__.pop('obj', None)
for k in 'module', 'parent', 'object_name':
if hasattr(im, k):
setattr(self, k, getattr(im, k))
file_path = getattr(im.module, '__file__', None)
try:
mod_all = inspect.getall(im.module)
except ValueError:
mod_all = None
if self.objtype == 'module':
self.props = _ModuleProperties(
obj_type=self.objtype,
name=im.object_name,
module_name=getattr(im, 'modname', self.modname),
docstring_lines=(),
file_path=Path(file_path) if file_path is not None else None,
all=tuple(mod_all) if mod_all is not None else None,
_obj=obj,
)
elif self.objtype in {'function', 'decorator'}:
obj_properties: set[_AutodocFuncProperty] = set()
if inspect.isstaticmethod(obj, cls=im.parent, name=im.object_name):
obj_properties.add('staticmethod')
if inspect.isclassmethod(obj):
obj_properties.add('classmethod')
self.props = _FunctionDefProperties(
obj_type=self.objtype,
name=im.object_name,
module_name=self.modname,
parts=tuple(self.objpath),
docstring_lines=(),
properties=frozenset(obj_properties),
_obj=obj,
)
else:
self.props = _ItemProperties(
obj_type=self.objtype,
name=im.object_name,
module_name=self.modname,
parts=tuple(self.objpath),
docstring_lines=(),
_obj=obj,
)
return True
return self._load_object_by_name() is not None
def get_real_modname(self) -> str:
"""Get the real module name of an object to document.
@@ -957,7 +848,7 @@ class Documenter:
member_documenters = [
(documenter, isattr)
for documenter, isattr in member_documenters
if documenter.parse_name() and documenter.import_object()
if documenter._load_object_by_name() is not None
]
member_documenters = self.sort_members(member_documenters, member_order)
@@ -1017,21 +908,7 @@ class Documenter:
True, only generate if the object is defined in the module name it is
imported from. If *all_members* is True, document all members.
"""
if not self.parse_name():
# 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)'
),
self.name,
type='autodoc',
)
return
# now, import the module and get object to document
if not self.import_object():
if self._load_object_by_name() is None:
return
self._generate(more_content, real_modname, check_module, all_members)
@@ -1174,16 +1051,6 @@ class ModuleDocumenter(Documenter):
# don't document submodules automatically
return False
def parse_name(self) -> bool:
ret = super().parse_name()
if self.args or self.retann:
logger.warning(
__('signature arguments or return annotation given for automodule %s'),
self.fullname,
type='autodoc',
)
return ret
def _module_all(self) -> Sequence[str] | None:
if self.object is not None and self.__all__ is None:
try:
@@ -1543,39 +1410,6 @@ class ClassDocumenter(Documenter):
isattr and isinstance(member, NewType | TypeVar)
)
def import_object(self, raiseerror: bool = False) -> bool:
try:
im = _import_class(
module_name=self.modname,
obj_path=self.objpath,
mock_imports=self.config.autodoc_mock_imports,
get_attr=self.get_attr,
)
except ImportError as exc:
if raiseerror:
raise
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False
self.object = obj = im.__dict__.pop('obj', None)
for k in 'module', 'parent', 'object_name', 'objpath', 'modname':
if hasattr(im, k):
setattr(self, k, getattr(im, k))
self.props = _ClassDefProperties(
obj_type=self.objtype, # type: ignore[arg-type]
name=im.object_name,
module_name=getattr(im, 'modname', self.modname),
parts=tuple(getattr(im, 'objpath', self.objpath)),
docstring_lines=(),
bases=getattr(obj, '__bases__', None),
_obj=obj,
_obj___name__=getattr(obj, '__name__', None),
)
return True
def _get_signature(self) -> tuple[Any | None, str | None, Signature | None]:
if isinstance(self.object, NewType | TypeVar):
# Suppress signature
@@ -2064,41 +1898,6 @@ class DataDocumenter(Documenter):
except PycodeError:
pass
def import_object(self, raiseerror: bool = False) -> bool:
try:
im = _import_assignment_data(
module_name=self.modname,
obj_path=self.objpath,
mock_imports=self.config.autodoc_mock_imports,
type_aliases=self.config.autodoc_type_aliases,
get_attr=self.get_attr,
)
except ImportError as exc:
if raiseerror:
raise
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False
self.object = obj = im.__dict__.pop('obj', None)
for k in 'module', 'parent', 'object_name':
if hasattr(im, k):
setattr(self, k, getattr(im, k))
self.props = _AssignStatementProperties(
obj_type=self.objtype, # type: ignore[arg-type]
name=im.object_name,
module_name=self.modname,
parts=tuple(self.objpath),
docstring_lines=(),
value=...,
annotation='',
class_var=False,
instance_var=False,
_obj=obj,
)
return True
def should_suppress_value_header(self) -> bool:
if self.object is UNINITIALIZED_ATTR:
return True
@@ -2212,43 +2011,6 @@ class MethodDocumenter(Documenter):
) -> bool:
return inspect.isroutine(member) and not isinstance(parent, ModuleDocumenter)
def import_object(self, raiseerror: bool = False) -> bool:
try:
im = _import_method(
module_name=self.modname,
obj_path=self.objpath,
member_order=self.member_order,
mock_imports=self.config.autodoc_mock_imports,
get_attr=self.get_attr,
)
except ImportError as exc:
if raiseerror:
raise
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False
self.object = obj = im.__dict__.pop('obj', None)
for k in 'module', 'parent', 'object_name', 'member_order':
if hasattr(im, k):
setattr(self, k, getattr(im, k))
obj_properties: set[_AutodocFuncProperty] = set()
if inspect.isstaticmethod(obj, cls=im.parent, name=im.object_name):
obj_properties.add('staticmethod')
if inspect.isclassmethod(obj):
obj_properties.add('classmethod')
self.props = _FunctionDefProperties(
obj_type=self.objtype, # type: ignore[arg-type]
name=im.object_name,
module_name=self.modname,
parts=tuple(self.objpath),
docstring_lines=(),
properties=frozenset(obj_properties),
_obj=obj,
)
return True
def format_args(self, **kwargs: Any) -> str:
if self.config.autodoc_typehints in {'none', 'description'}:
kwargs.setdefault('show_annotation', False)
@@ -2540,41 +2302,6 @@ class AttributeDocumenter(Documenter):
# Failed to set __annotations__ (built-in, extensions, etc.)
pass
def import_object(self, raiseerror: bool = False) -> bool:
try:
im = _import_assignment_attribute(
module_name=self.modname,
obj_path=self.objpath,
mock_imports=self.config.autodoc_mock_imports,
type_aliases=self.config.autodoc_type_aliases,
get_attr=self.get_attr,
)
except ImportError as exc:
if raiseerror:
raise
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False
self.object = obj = im.__dict__.pop('obj', None)
for k in 'module', 'parent', 'object_name':
if hasattr(im, k):
setattr(self, k, getattr(im, k))
self.props = _AssignStatementProperties(
obj_type=self.objtype, # type: ignore[arg-type]
name=im.object_name,
module_name=self.modname,
parts=tuple(self.objpath),
docstring_lines=(),
value=...,
annotation='',
class_var=False,
instance_var=False,
_obj=obj,
)
return True
@property
def _is_non_data_descriptor(self) -> bool:
return not inspect.isattributedescriptor(self.object)
@@ -2744,42 +2471,6 @@ class PropertyDocumenter(Documenter):
else:
return False
def import_object(self, raiseerror: bool = False) -> bool:
try:
im = _import_property(
module_name=self.modname,
obj_path=self.objpath,
mock_imports=self.config.autodoc_mock_imports,
get_attr=self.get_attr,
)
except ImportError as exc:
if raiseerror:
raise
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False
if im is None:
return False
self.object = obj = im.__dict__.pop('obj', None)
for k in 'module', 'parent', 'object_name', 'isclassmethod':
if hasattr(im, k):
setattr(self, k, getattr(im, k))
obj_properties: set[_AutodocFuncProperty] = set()
if getattr(im, 'isclassmethod', False):
obj_properties.add('classmethod')
self.props = _FunctionDefProperties(
obj_type=self.objtype, # type: ignore[arg-type]
name=im.object_name,
module_name=self.modname,
parts=tuple(self.objpath),
docstring_lines=(),
properties=frozenset(obj_properties),
_obj=obj,
)
return True
def format_args(self, **kwargs: Any) -> str:
func = self._get_property_getter()
if func is None:
+444 -207
View File
@@ -16,6 +16,13 @@ from pathlib import Path
from typing import TYPE_CHECKING, NamedTuple, NewType, TypeVar
from sphinx.errors import PycodeError
from sphinx.ext.autodoc._property_types import (
_AssignStatementProperties,
_ClassDefProperties,
_FunctionDefProperties,
_ItemProperties,
_ModuleProperties,
)
from sphinx.ext.autodoc._sentinels import (
INSTANCE_ATTR,
RUNTIME_INSTANCE_ATTRIBUTE,
@@ -23,6 +30,7 @@ from sphinx.ext.autodoc._sentinels import (
UNINITIALIZED_ATTR,
)
from sphinx.ext.autodoc.mock import ismock, mock, undecorate
from sphinx.locale import __
from sphinx.pycode import ModuleAnalyzer
from sphinx.util import inspect, logging
from sphinx.util.inspect import (
@@ -42,7 +50,9 @@ if TYPE_CHECKING:
from types import ModuleType
from typing import Any, Protocol
from sphinx.environment import BuildEnvironment, _CurrentDocument
from sphinx.ext.autodoc import ObjectMember
from sphinx.ext.autodoc._property_types import _AutodocFuncProperty, _AutodocObjType
class _AttrGetter(Protocol):
def __call__(self, obj: Any, name: str, default: Any = ..., /) -> Any: ...
@@ -65,16 +75,6 @@ class _ImportedObject:
#: object to document
obj: Any
# ClassDocumenter
objpath: list[str]
modname: str
# MethodDocumenter
member_order: int
# PropertyDocumenter
isclassmethod: bool
def __init__(
self,
*,
@@ -613,218 +613,70 @@ def _import_object(
raise
def _import_class(
def _import_data_declaration(
*,
module_name: str,
obj_path: Sequence[str],
mock_imports: list[str],
get_attr: _AttrGetter = safe_getattr,
) -> _ImportedObject:
im = _import_object(
module_name=module_name,
obj_path=obj_path,
mock_imports=mock_imports,
get_attr=get_attr,
)
if isinstance(im.obj, NewType | TypeVar):
obj_module_name = getattr(im.obj, '__module__', module_name)
if obj_module_name != module_name and module_name.startswith(obj_module_name):
bases = module_name[len(obj_module_name) :].strip('.').split('.')
im.objpath = bases + list(obj_path)
im.modname = obj_module_name
return im
type_aliases: dict[str, Any] | None,
) -> _ImportedObject | None:
# annotation only instance variable (PEP-526)
try:
with mock(mock_imports):
parent = import_module(module_name)
annotations = get_type_hints(parent, None, type_aliases, include_extras=True)
if obj_path[-1] in annotations:
im = _ImportedObject(
parent=parent,
obj=UNINITIALIZED_ATTR,
)
return im
except ImportError:
pass
return None
def _import_method(
*,
module_name: str,
obj_path: Sequence[str],
member_order: int,
mock_imports: list[str],
get_attr: _AttrGetter = safe_getattr,
) -> _ImportedObject:
im = _import_object(
module_name=module_name,
obj_path=obj_path,
mock_imports=mock_imports,
get_attr=get_attr,
)
# to distinguish classmethod/staticmethod
obj = im.parent.__dict__.get(im.object_name, im.obj)
if inspect.isstaticmethod(obj, cls=im.parent, name=im.object_name):
# document static members before regular methods
im.member_order = member_order - 1
elif inspect.isclassmethod(obj):
# document class methods before static methods as
# they usually behave as alternative constructors
im.member_order = member_order - 2
return im
def _import_property(
def _import_attribute_declaration(
*,
module_name: str,
obj_path: Sequence[str],
mock_imports: list[str],
type_aliases: dict[str, Any] | None,
get_attr: _AttrGetter = safe_getattr,
) -> _ImportedObject | None:
im = _import_object(
module_name=module_name,
obj_path=obj_path,
mock_imports=mock_imports,
get_attr=get_attr,
)
if not inspect.isproperty(im.obj):
# Support for class properties. Note: these only work on Python 3.9.
__dict__ = safe_getattr(im.parent, '__dict__', {})
obj = __dict__.get(obj_path[-1])
if isinstance(obj, classmethod) and inspect.isproperty(obj.__func__):
im.obj = obj.__func__
im.isclassmethod = True
# Support runtime & uninitialized instance attributes.
#
# The former are defined in __init__() methods with doc-comments.
# The latter are PEP-526 style annotation only annotations.
#
# class Foo:
# attr: int #: uninitialized attribute
#
# def __init__(self):
# self.attr = None #: runtime attribute
try:
with mock(mock_imports):
ret = _import_from_module_and_path(
module_name=module_name, obj_path=obj_path[:-1], get_attr=get_attr
)
parent = ret.obj
if _is_runtime_instance_attribute(parent=parent, obj_path=obj_path):
im = _ImportedObject(
parent=parent,
obj=RUNTIME_INSTANCE_ATTRIBUTE,
)
return im
else:
return None
return im
def _import_assignment_data(
*,
module_name: str,
obj_path: Sequence[str],
mock_imports: list[str],
type_aliases: dict[str, Any] | None,
get_attr: _AttrGetter = safe_getattr,
) -> _ImportedObject:
import_failed = True
try:
with mock(mock_imports):
im = _import_from_module_and_path(
module_name=module_name, obj_path=obj_path, get_attr=get_attr
elif _is_uninitialized_instance_attribute(
parent=parent, obj_path=obj_path, type_aliases=type_aliases
):
im = _ImportedObject(
parent=parent,
obj=UNINITIALIZED_ATTR,
)
if ismock(im.obj):
im.obj = undecorate(im.obj)
import_failed = False
except ImportError as exc:
# annotation only instance variable (PEP-526)
try:
with mock(mock_imports):
parent = import_module(module_name)
annotations = get_type_hints(
parent, None, type_aliases, include_extras=True
)
if obj_path[-1] in annotations:
im = _ImportedObject(
parent=parent,
obj=UNINITIALIZED_ATTR,
)
import_failed = False
except ImportError:
pass
if import_failed:
raise
# Update __annotations__ to support type_comment and so on
annotations = dict(inspect.getannotations(im.parent))
im.parent.__annotations__ = annotations
try:
analyzer = ModuleAnalyzer.for_module(module_name)
analyzer.analyze()
for (classname, attrname), annotation in analyzer.annotations.items():
if not classname and attrname not in annotations:
annotations[attrname] = annotation
except PycodeError:
return im
except ImportError:
pass
return im
def _import_assignment_attribute(
*,
module_name: str,
obj_path: Sequence[str],
mock_imports: list[str],
type_aliases: dict[str, Any] | None,
get_attr: _AttrGetter = safe_getattr,
) -> _ImportedObject:
import_failed = True
try:
with mock(mock_imports):
im = _import_from_module_and_path(
module_name=module_name, obj_path=obj_path, get_attr=get_attr
)
if ismock(im.obj):
im.obj = undecorate(im.obj)
import_failed = False
except ImportError as exc:
# Support runtime & uninitialized instance attributes.
#
# The former are defined in __init__() methods with doc-comments.
# The latter are PEP-526 style annotation only annotations.
#
# class Foo:
# attr: int #: uninitialized attribute
#
# def __init__(self):
# self.attr = None #: runtime attribute
try:
with mock(mock_imports):
ret = _import_from_module_and_path(
module_name=module_name, obj_path=obj_path[:-1], get_attr=get_attr
)
parent = ret.obj
if _is_runtime_instance_attribute(parent=parent, obj_path=obj_path):
im = _ImportedObject(
parent=parent,
obj=RUNTIME_INSTANCE_ATTRIBUTE,
)
import_failed = False
elif _is_uninitialized_instance_attribute(
parent=parent, obj_path=obj_path, type_aliases=type_aliases
):
im = _ImportedObject(
parent=parent,
obj=UNINITIALIZED_ATTR,
)
import_failed = False
except ImportError:
pass
if import_failed:
raise
if _is_slots_attribute(parent=im.parent, obj_path=obj_path):
im.obj = SLOTS_ATTR
elif inspect.isenumattribute(im.obj):
im.obj = im.obj.value
if im.parent:
# Update __annotations__ to support type_comment and so on.
try:
annotations = dict(inspect.getannotations(im.parent))
im.parent.__annotations__ = annotations
for cls in inspect.getmro(im.parent):
try:
module = safe_getattr(cls, '__module__')
qualname = safe_getattr(cls, '__qualname__')
analyzer = ModuleAnalyzer.for_module(module)
analyzer.analyze()
anns = analyzer.annotations
for (classname, attrname), annotation in anns.items():
if classname == qualname and attrname not in annotations:
annotations[attrname] = annotation
except (AttributeError, PycodeError):
pass
except (AttributeError, TypeError):
# Failed to set __annotations__ (built-in, extensions, etc.)
pass
return im
return None
def _is_runtime_instance_attribute(*, parent: Any, obj_path: Sequence[str]) -> bool:
@@ -895,3 +747,388 @@ def _is_slots_attribute(*, parent: Any, obj_path: Sequence[str]) -> bool:
return False
except (ValueError, TypeError):
return False
def _load_object_by_name(
*,
name: str,
objtype: _AutodocObjType,
mock_imports: list[str],
type_aliases: dict[str, Any] | None,
current_document: _CurrentDocument,
env: BuildEnvironment,
get_attr: _AttrGetter,
) -> tuple[_ItemProperties, str | None, str | None, ModuleType | None, Any] | None:
"""Import and load the object given by *name*."""
parsed = _parse_name(
name=name,
objtype=objtype,
current_document=current_document,
env=env,
)
if parsed is None:
return None
module_name, parts, args, retann = parsed
# Import the module and get the object to document
try:
im = _import_object(
module_name=module_name,
obj_path=parts,
mock_imports=mock_imports,
get_attr=get_attr,
)
except ImportError as exc:
if objtype == 'data':
im_ = _import_data_declaration(
module_name=module_name,
obj_path=parts,
mock_imports=mock_imports,
type_aliases=type_aliases,
)
elif objtype == 'attribute':
im_ = _import_attribute_declaration(
module_name=module_name,
obj_path=parts,
mock_imports=mock_imports,
type_aliases=type_aliases,
get_attr=get_attr,
)
else:
im_ = None
if im_ is None:
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
env.note_reread()
return None
else:
im = im_
# Assemble object properties from the imported object.
props: _ItemProperties
module = im.module
parent = im.parent
object_name = im.object_name
obj = im.obj
obj_properties: set[_AutodocFuncProperty] = set()
if objtype == 'module':
file_path = getattr(module, '__file__', None)
try:
mod_all = inspect.getall(module)
except ValueError:
mod_all = None
props = _ModuleProperties(
obj_type=objtype,
name=object_name,
module_name=module_name,
docstring_lines=(),
file_path=Path(file_path) if file_path is not None else None,
all=tuple(mod_all) if mod_all is not None else None,
_obj=obj,
)
elif objtype in {'class', 'exception'}:
if isinstance(obj, NewType | TypeVar):
obj_module_name = getattr(obj, '__module__', module_name)
if obj_module_name != module_name and module_name.startswith(
obj_module_name
):
bases = module_name[len(obj_module_name) :].strip('.').split('.')
parts = tuple(bases) + parts
module_name = obj_module_name
props = _ClassDefProperties(
obj_type=objtype, # type: ignore[arg-type]
name=object_name,
module_name=module_name,
parts=parts,
docstring_lines=(),
bases=getattr(obj, '__bases__', None),
_obj=obj,
_obj___name__=getattr(obj, '__name__', None),
)
elif objtype in {'function', 'decorator'}:
if inspect.isstaticmethod(obj, cls=parent, name=object_name):
obj_properties.add('staticmethod')
if inspect.isclassmethod(obj):
obj_properties.add('classmethod')
props = _FunctionDefProperties(
obj_type=objtype, # type: ignore[arg-type]
name=object_name,
module_name=module_name,
parts=parts,
docstring_lines=(),
properties=frozenset(obj_properties),
_obj=obj,
)
elif objtype == 'method':
# to distinguish classmethod/staticmethod
obj_ = parent.__dict__.get(object_name, obj)
if inspect.isstaticmethod(obj_, cls=parent, name=object_name):
obj_properties.add('staticmethod')
elif inspect.isclassmethod(obj_):
obj_properties.add('classmethod')
props = _FunctionDefProperties(
obj_type=objtype,
name=object_name,
module_name=module_name,
parts=parts,
docstring_lines=(),
properties=frozenset(obj_properties),
_obj=obj,
)
elif objtype == 'property':
if not inspect.isproperty(obj):
# Support for class properties. Note: these only work on Python 3.9.
__dict__ = safe_getattr(parent, '__dict__', {})
obj = __dict__.get(parts[-1])
if isinstance(obj, classmethod) and inspect.isproperty(obj.__func__):
obj = obj.__func__
obj_properties.add('classmethod')
else:
return None
props = _FunctionDefProperties(
obj_type=objtype,
name=object_name,
module_name=module_name,
parts=parts,
docstring_lines=(),
properties=frozenset(obj_properties),
_obj=obj,
)
elif objtype == 'data':
# Update __annotations__ to support type_comment and so on
annotations = dict(inspect.getannotations(parent))
parent.__annotations__ = annotations
try:
analyzer = ModuleAnalyzer.for_module(module_name)
analyzer.analyze()
for (
classname,
attrname,
), annotation in analyzer.annotations.items():
if not classname and attrname not in annotations:
annotations[attrname] = annotation
except PycodeError:
pass
props = _AssignStatementProperties(
obj_type=objtype,
name=object_name,
module_name=module_name,
parts=parts,
docstring_lines=(),
value=...,
annotation='',
class_var=False,
instance_var=False,
_obj=obj,
)
elif objtype == 'attribute':
if _is_slots_attribute(parent=parent, obj_path=parts):
obj = SLOTS_ATTR
elif inspect.isenumattribute(obj):
obj = obj.value
if parent:
# Update __annotations__ to support type_comment and so on.
try:
annotations = dict(inspect.getannotations(parent))
parent.__annotations__ = annotations
for cls in inspect.getmro(parent):
try:
module = safe_getattr(cls, '__module__')
qualname = safe_getattr(cls, '__qualname__')
analyzer = ModuleAnalyzer.for_module(module)
analyzer.analyze()
anns = analyzer.annotations
for (classname, attrname), annotation in anns.items():
if classname == qualname and attrname not in annotations:
annotations[attrname] = annotation
except (AttributeError, PycodeError):
pass
except (AttributeError, TypeError):
# Failed to set __annotations__ (built-in, extensions, etc.)
pass
props = _AssignStatementProperties(
obj_type=objtype,
name=object_name,
module_name=module_name,
parts=parts,
docstring_lines=(),
value=...,
annotation='',
class_var=False,
instance_var=False,
_obj=obj,
)
else:
props = _ItemProperties(
obj_type=objtype,
name=object_name,
module_name=module_name,
parts=parts,
docstring_lines=(),
_obj=obj,
)
return props, args, retann, module, parent
def _parse_name(
*,
name: str,
objtype: _AutodocObjType,
current_document: _CurrentDocument,
env: BuildEnvironment,
) -> tuple[str, tuple[str, ...], str | None, str | None] | None:
"""Parse *name* into module name, path, arguments, and return annotation."""
from sphinx.ext.autodoc._documenters import py_ext_sig_re
# 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()
# 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=env.ref_context.get('py:module'),
ref_context_py_class=env.ref_context.get('py:class', ''),
)
if resolved is None:
msg = 'must be implemented in subclasses'
raise NotImplementedError(msg)
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: str,
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'}:
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
+60 -57
View File
@@ -27,6 +27,7 @@ from sphinx.ext.autodoc._sentinels import ALL
# NEVER import these objects from sphinx.ext.autodoc directly
from sphinx.ext.autodoc.directive import DocumenterBridge
from sphinx.ext.autodoc.importer import _parse_name
from tests.test_extensions.autodoc_util import do_autodoc
@@ -80,58 +81,54 @@ processed_signatures = []
@pytest.mark.sphinx('html', testroot='root')
def test_parse_name(app):
def verify(objtype, name, result):
inst = app.registry.documenters[objtype](directive, name)
assert inst.parse_name()
assert (inst.modname, inst.objpath, inst.args, inst.retann) == result
env = app.env
current_document = env.current_document
directive = make_directive_bridge(app.env)
def parse(objtype, name):
parsed = _parse_name(
name=name, objtype=objtype, current_document=current_document, env=env
)
if parsed is None:
return None
module_name, parts, args, retann = parsed
return module_name, list(parts), args, retann
# for modules
verify('module', 'test_ext_autodoc', ('test_ext_autodoc', [], None, None))
verify('module', 'test.test_ext_autodoc', ('test.test_ext_autodoc', [], None, None))
verify('module', 'test(arg)', ('test', [], 'arg', None))
parsed = parse('module', 'test_ext_autodoc')
assert parsed == ('test_ext_autodoc', [], None, None)
parsed = parse('module', 'test.test_ext_autodoc')
assert parsed == ('test.test_ext_autodoc', [], None, None)
parsed = parse('module', 'test(arg)')
assert parsed is None
assert 'signature arguments' in app.warning.getvalue()
# for functions/classes
verify(
'function',
'test_ext_autodoc.raises',
('test_ext_autodoc', ['raises'], None, None),
)
verify(
'function',
'test_ext_autodoc.raises(exc) -> None',
('test_ext_autodoc', ['raises'], 'exc', 'None'),
)
directive.env.current_document.autodoc_module = 'test_ext_autodoc'
verify('function', 'raises', ('test_ext_autodoc', ['raises'], None, None))
directive.env.current_document.autodoc_module = ''
parsed = parse('function', 'test_ext_autodoc.raises')
assert parsed == ('test_ext_autodoc', ['raises'], None, None)
parsed = parse('function', 'test_ext_autodoc.raises(exc) -> None')
assert parsed == ('test_ext_autodoc', ['raises'], 'exc', 'None')
current_document.autodoc_module = 'test_ext_autodoc'
parsed = parse('function', 'raises')
assert parsed == ('test_ext_autodoc', ['raises'], None, None)
current_document.autodoc_module = ''
directive.env.ref_context['py:module'] = 'test_ext_autodoc'
verify('function', 'raises', ('test_ext_autodoc', ['raises'], None, None))
verify('class', 'Base', ('test_ext_autodoc', ['Base'], None, None))
env.ref_context['py:module'] = 'test_ext_autodoc'
parsed = parse('function', 'raises')
assert parsed == ('test_ext_autodoc', ['raises'], None, None)
parsed = parse('class', 'Base')
assert parsed == ('test_ext_autodoc', ['Base'], None, None)
# for members
directive.env.ref_context['py:module'] = 'sphinx.testing.util'
verify(
'method',
'SphinxTestApp.cleanup',
('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None),
)
directive.env.ref_context['py:module'] = 'sphinx.testing.util'
directive.env.ref_context['py:class'] = 'Foo'
directive.env.current_document.autodoc_class = 'SphinxTestApp'
verify(
'method',
'cleanup',
('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None),
)
verify(
'method',
'SphinxTestApp.cleanup',
('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None),
)
env.ref_context['py:module'] = 'sphinx.testing.util'
parsed = parse('method', 'SphinxTestApp.cleanup')
assert parsed == ('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None)
env.ref_context['py:module'] = 'sphinx.testing.util'
env.ref_context['py:class'] = 'Foo'
current_document.autodoc_class = 'SphinxTestApp'
parsed = parse('method', 'cleanup')
assert parsed == ('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None)
parsed = parse('method', 'SphinxTestApp.cleanup')
assert parsed == ('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None)
@pytest.mark.sphinx('html', testroot='root')
@@ -431,24 +428,30 @@ def test_get_doc(app):
assert getdocl('function', J().foo) == ['Method docstring']
class _MyDocumenter(ModuleLevelDocumenter):
objtype = 'integer'
directivetype = 'integer'
priority = 100
@classmethod
def can_document_member(cls, member, membername, isattr, parent):
return isinstance(member, int)
def document_members(self, all_members=False):
return
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_new_documenter(app):
class MyDocumenter(ModuleLevelDocumenter):
objtype = 'integer'
directivetype = 'integer'
priority = 100
@classmethod
def can_document_member(cls, member, membername, isattr, parent):
return isinstance(member, int)
def document_members(self, all_members=False):
return
app.add_autodocumenter(MyDocumenter)
app.add_autodocumenter(_MyDocumenter)
options = {'members': 'integer'}
actual = do_autodoc(app, 'module', 'target', options)
with pytest.raises(
NotImplementedError, match=r'^must be implemented in subclasses$'
):
# TODO: Fix! Perhaps add a way to signal module/class-level?
actual = do_autodoc(app, 'module', 'target', options)
return
assert list(actual) == [
'',
'.. py:module:: target',