summaryrefslogtreecommitdiff
path: root/sphinx/ext/autodoc/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/ext/autodoc/__init__.py')
-rw-r--r--sphinx/ext/autodoc/__init__.py364
1 files changed, 279 insertions, 85 deletions
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py
index 98a3cc85d..083f2aaa7 100644
--- a/sphinx/ext/autodoc/__init__.py
+++ b/sphinx/ext/autodoc/__init__.py
@@ -13,9 +13,11 @@
import importlib
import re
import warnings
-from inspect import Parameter
+from inspect import Parameter, Signature
from types import ModuleType
-from typing import Any, Callable, Dict, Iterator, List, Sequence, Set, Tuple, Type, Union
+from typing import (
+ Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union
+)
from typing import TYPE_CHECKING
from docutils.statemachine import StringList
@@ -91,6 +93,16 @@ def inherited_members_option(arg: Any) -> Union[object, Set[str]]:
return arg
+def member_order_option(arg: Any) -> Optional[str]:
+ """Used to convert the :members: option to auto directives."""
+ if arg is None:
+ return None
+ elif arg in ('alphabetical', 'bysource', 'groupwise'):
+ return arg
+ else:
+ raise ValueError(__('invalid value for member-order option: %s') % arg)
+
+
SUPPRESS = object()
@@ -379,6 +391,17 @@ class Documenter:
# directives of course)
return '.'.join(self.objpath) or self.modname
+ def _call_format_args(self, **kwargs: Any) -> str:
+ if kwargs:
+ try:
+ return self.format_args(**kwargs)
+ except TypeError:
+ # avoid chaining exceptions, by putting nothing here
+ pass
+
+ # retry without arguments for old documenters
+ return self.format_args()
+
def format_signature(self, **kwargs: Any) -> str:
"""Format the signature (arguments and return annotation) of the object.
@@ -387,21 +410,22 @@ class Documenter:
if self.args is not None:
# signature given explicitly
args = "(%s)" % self.args
+ retann = self.retann
else:
# try to introspect the signature
try:
- try:
- args = self.format_args(**kwargs)
- except TypeError:
- # retry without arguments for old documenters
- args = self.format_args()
+ retann = None
+ args = self._call_format_args(**kwargs)
+ if args:
+ matched = re.match(r'^(\(.*\))\s+->\s+(.*)$', args)
+ if matched:
+ args = matched.group(1)
+ retann = matched.group(2)
except Exception:
logger.warning(__('error while formatting arguments for %s:') %
self.fullname, type='autodoc', exc_info=True)
args = None
- retann = self.retann
-
result = self.env.events.emit_firstresult('autodoc-process-signature',
self.objtype, self.fullname,
self.object, self.options, args, retann)
@@ -516,12 +540,12 @@ class Documenter:
else:
logger.warning(__('missing attribute %s in object %s') %
(name, self.fullname), type='autodoc')
- return False, sorted(selected)
+ return False, selected
elif self.options.inherited_members:
- return False, sorted((m.name, m.value) for m in members.values())
+ return False, [(m.name, m.value) for m in members.values()]
else:
- return False, sorted((m.name, m.value) for m in members.values()
- if m.directly_defined)
+ return False, [(m.name, m.value) for m in members.values()
+ if m.directly_defined]
def filter_members(self, members: List[Tuple[str, Any]], want_all: bool
) -> List[Tuple[str, Any, bool]]:
@@ -691,20 +715,9 @@ class Documenter:
'.'.join(self.objpath + [mname])
documenter = classes[-1](self.directive, full_mname, self.indent)
memberdocumenters.append((documenter, isattr))
- member_order = self.options.member_order or \
- self.env.config.autodoc_member_order
- if member_order == 'groupwise':
- # sort by group; relies on stable sort to keep items in the
- # same group sorted alphabetically
- memberdocumenters.sort(key=lambda e: e[0].member_order)
- elif member_order == 'bysource' and self.analyzer:
- # sort by source order, by virtue of the module analyzer
- tagorder = self.analyzer.tagorder
- def keyfunc(entry: Tuple[Documenter, bool]) -> int:
- fullname = entry[0].name.split('::')[1]
- return tagorder.get(fullname, len(tagorder))
- memberdocumenters.sort(key=keyfunc)
+ member_order = self.options.member_order or self.env.config.autodoc_member_order
+ memberdocumenters = self.sort_members(memberdocumenters, member_order)
for documenter, isattr in memberdocumenters:
documenter.generate(
@@ -715,6 +728,31 @@ class Documenter:
self.env.temp_data['autodoc:module'] = None
self.env.temp_data['autodoc:class'] = None
+ def sort_members(self, documenters: List[Tuple["Documenter", bool]],
+ order: str) -> List[Tuple["Documenter", bool]]:
+ """Sort the given member list."""
+ if order == 'groupwise':
+ # sort by group; alphabetically within groups
+ documenters.sort(key=lambda e: (e[0].member_order, e[0].name))
+ elif order == 'bysource':
+ if self.analyzer:
+ # sort by source order, by virtue of the module analyzer
+ tagorder = self.analyzer.tagorder
+
+ def keyfunc(entry: Tuple[Documenter, bool]) -> int:
+ fullname = entry[0].name.split('::')[1]
+ return tagorder.get(fullname, len(tagorder))
+ documenters.sort(key=keyfunc)
+ else:
+ # Assume that member discovery order matches source order.
+ # This is a reasonable assumption in Python 3.6 and up, where
+ # module.__dict__ is insertion-ordered.
+ pass
+ else: # alphabetical
+ documenters.sort(key=lambda e: e[0].name)
+
+ return documenters
+
def generate(self, more_content: Any = None, real_modname: str = None,
check_module: bool = False, all_members: bool = False) -> None:
"""Generate reST for the object given by *self.name*, and possibly for
@@ -812,7 +850,7 @@ class ModuleDocumenter(Documenter):
'noindex': bool_option, 'inherited-members': inherited_members_option,
'show-inheritance': bool_option, 'synopsis': identity,
'platform': identity, 'deprecated': bool_option,
- 'member-order': identity, 'exclude-members': members_set_option,
+ 'member-order': member_order_option, 'exclude-members': members_set_option,
'private-members': bool_option, 'special-members': members_option,
'imported-members': bool_option, 'ignore-module-all': bool_option
} # type: Dict[str, Callable]
@@ -820,6 +858,7 @@ class ModuleDocumenter(Documenter):
def __init__(self, *args: Any) -> None:
super().__init__(*args)
merge_special_members_option(self.options)
+ self.__all__ = None
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
@@ -842,6 +881,30 @@ class ModuleDocumenter(Documenter):
type='autodoc')
return ret
+ def import_object(self) -> Any:
+ def is_valid_module_all(__all__: Any) -> bool:
+ """Check the given *__all__* is valid for a module."""
+ if (isinstance(__all__, (list, tuple)) and
+ all(isinstance(e, str) for e in __all__)):
+ return True
+ else:
+ return False
+
+ ret = super().import_object()
+
+ if not self.options.ignore_module_all:
+ __all__ = getattr(self.object, '__all__', None)
+ if is_valid_module_all(__all__):
+ # valid __all__ found. copy it to self.__all__
+ self.__all__ = __all__
+ elif __all__:
+ # invalid __all__ found.
+ logger.warning(__('__all__ should be a list of strings, not %r '
+ '(in module %s) -- ignoring __all__') %
+ (__all__, self.fullname), type='autodoc')
+
+ return ret
+
def add_directive_header(self, sig: str) -> None:
Documenter.add_directive_header(self, sig)
@@ -857,24 +920,12 @@ class ModuleDocumenter(Documenter):
def get_object_members(self, want_all: bool) -> Tuple[bool, List[Tuple[str, Any]]]:
if want_all:
- if (self.options.ignore_module_all or not
- hasattr(self.object, '__all__')):
+ if self.__all__:
+ memberlist = self.__all__
+ else:
# for implicit module members, check __module__ to avoid
# documenting imported objects
return True, get_module_members(self.object)
- else:
- memberlist = self.object.__all__
- # Sometimes __all__ is broken...
- if not isinstance(memberlist, (list, tuple)) or not \
- all(isinstance(entry, str) for entry in memberlist):
- logger.warning(
- __('__all__ should be a list of strings, not %r '
- '(in module %s) -- ignoring __all__') %
- (memberlist, self.fullname),
- type='autodoc'
- )
- # fall back to all members
- return True, get_module_members(self.object)
else:
memberlist = self.options.members or []
ret = []
@@ -890,6 +941,25 @@ class ModuleDocumenter(Documenter):
)
return False, ret
+ def sort_members(self, documenters: List[Tuple["Documenter", bool]],
+ order: str) -> List[Tuple["Documenter", bool]]:
+ if order == 'bysource' and self.__all__:
+ # Sort alphabetically first (for members not listed on the __all__)
+ documenters.sort(key=lambda e: e[0].name)
+
+ # Sort by __all__
+ def keyfunc(entry: Tuple[Documenter, bool]) -> int:
+ name = entry[0].name.split('::')[1]
+ if name in self.__all__:
+ return self.__all__.index(name)
+ else:
+ return len(self.__all__)
+ documenters.sort(key=keyfunc)
+
+ return documenters
+ else:
+ return super().sort_members(documenters, order)
+
class ModuleLevelDocumenter(Documenter):
"""
@@ -961,41 +1031,71 @@ class DocstringSignatureMixin:
Mixin for FunctionDocumenter and MethodDocumenter to provide the
feature of reading the signature from the docstring.
"""
+ _new_docstrings = None # type: List[List[str]]
+ _signatures = None # type: List[str]
def _find_signature(self) -> Tuple[str, str]:
+ # candidates of the object name
+ valid_names = [self.objpath[-1]] # type: ignore
+ if isinstance(self, ClassDocumenter):
+ valid_names.append('__init__')
+ if hasattr(self.object, '__mro__'):
+ valid_names.extend(cls.__name__ for cls in self.object.__mro__)
+
docstrings = self.get_doc()
self._new_docstrings = docstrings[:]
+ self._signatures = []
result = None
for i, doclines in enumerate(docstrings):
- # no lines in docstring, no match
- if not doclines:
- continue
- # match first line of docstring against signature RE
- match = py_ext_sig_re.match(doclines[0])
- if not match:
- continue
- exmod, path, base, args, retann = match.groups()
- # the base name must match ours
- valid_names = [self.objpath[-1]] # type: ignore
- if isinstance(self, ClassDocumenter):
- valid_names.append('__init__')
- if hasattr(self.object, '__mro__'):
- valid_names.extend(cls.__name__ for cls in self.object.__mro__)
- if base not in valid_names:
- continue
- # re-prepare docstring to ignore more leading indentation
- tab_width = self.directive.state.document.settings.tab_width # type: ignore
- self._new_docstrings[i] = prepare_docstring('\n'.join(doclines[1:]),
- tabsize=tab_width)
- result = args, retann
- # don't look any further
- break
+ for j, line in enumerate(doclines):
+ if not line:
+ # no lines in docstring, no match
+ break
+
+ if line.endswith('\\'):
+ multiline = True
+ line = line.rstrip('\\').rstrip()
+ else:
+ multiline = False
+
+ # match first line of docstring against signature RE
+ match = py_ext_sig_re.match(line)
+ if not match:
+ continue
+ exmod, path, base, args, retann = match.groups()
+
+ # the base name must match ours
+ if base not in valid_names:
+ continue
+
+ # re-prepare docstring to ignore more leading indentation
+ tab_width = self.directive.state.document.settings.tab_width # type: ignore
+ self._new_docstrings[i] = prepare_docstring('\n'.join(doclines[j + 1:]),
+ tabsize=tab_width)
+
+ if result is None:
+ # first signature
+ result = args, retann
+ else:
+ # subsequent signatures
+ self._signatures.append("(%s) -> %s" % (args, retann))
+
+ if multiline:
+ # the signature have multiple signatures on docstring
+ continue
+ else:
+ # don't look any further
+ break
+
+ if result:
+ # finish the loop when signature found
+ break
+
return result
def get_doc(self, ignore: int = None) -> List[List[str]]:
- lines = getattr(self, '_new_docstrings', None)
- if lines is not None:
- return lines
+ if self._new_docstrings is not None:
+ return self._new_docstrings
return super().get_doc(ignore) # type: ignore
def format_signature(self, **kwargs: Any) -> str:
@@ -1005,7 +1105,11 @@ class DocstringSignatureMixin:
result = self._find_signature()
if result is not None:
self.args, self.retann = result
- return super().format_signature(**kwargs) # type: ignore
+ sig = super().format_signature(**kwargs) # type: ignore
+ if self._signatures:
+ return "\n".join([sig] + self._signatures)
+ else:
+ return sig
class DocstringStripSignatureMixin(DocstringSignatureMixin):
@@ -1087,6 +1191,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
documenter = FunctionDocumenter(self.directive, '')
documenter.object = func
+ documenter.objpath = [None]
sigs.append(documenter.format_signature())
return "\n".join(sigs)
@@ -1128,6 +1233,14 @@ class DecoratorDocumenter(FunctionDocumenter):
return None
+# Types which have confusing metaclass signatures it would be best not to show.
+# These are listed by name, rather than storing the objects themselves, to avoid
+# needing to import the modules.
+_METACLASS_CALL_BLACKLIST = [
+ 'enum.EnumMeta.__call__',
+]
+
+
class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: ignore
"""
Specialized Documenter subclass for classes.
@@ -1137,7 +1250,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
option_spec = {
'members': members_option, 'undoc-members': bool_option,
'noindex': bool_option, 'inherited-members': inherited_members_option,
- 'show-inheritance': bool_option, 'member-order': identity,
+ 'show-inheritance': bool_option, 'member-order': member_order_option,
'exclude-members': members_set_option,
'private-members': bool_option, 'special-members': members_option,
} # type: Dict[str, Callable]
@@ -1162,27 +1275,83 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
self.doc_as_attr = True
return ret
+ def _get_signature(self) -> Optional[Signature]:
+ def get_user_defined_function_or_method(obj: Any, attr: str) -> Any:
+ """ Get the `attr` function or method from `obj`, if it is user-defined. """
+ if inspect.is_builtin_class_method(obj, attr):
+ return None
+ attr = self.get_attr(obj, attr, None)
+ if not (inspect.ismethod(attr) or inspect.isfunction(attr)):
+ return None
+ return attr
+
+ # This sequence is copied from inspect._signature_from_callable.
+ # ValueError means that no signature could be found, so we keep going.
+
+ # First, let's see if it has an overloaded __call__ defined
+ # in its metaclass
+ call = get_user_defined_function_or_method(type(self.object), '__call__')
+
+ if call is not None:
+ if "{0.__module__}.{0.__qualname__}".format(call) in _METACLASS_CALL_BLACKLIST:
+ call = None
+
+ if call is not None:
+ self.env.app.emit('autodoc-before-process-signature', call, True)
+ try:
+ return inspect.signature(call, bound_method=True)
+ except ValueError:
+ pass
+
+ # Now we check if the 'obj' class has a '__new__' method
+ new = get_user_defined_function_or_method(self.object, '__new__')
+ if new is not None:
+ self.env.app.emit('autodoc-before-process-signature', new, True)
+ try:
+ return inspect.signature(new, bound_method=True)
+ except ValueError:
+ pass
+
+ # Finally, we should have at least __init__ implemented
+ init = get_user_defined_function_or_method(self.object, '__init__')
+ if init is not None:
+ self.env.app.emit('autodoc-before-process-signature', init, True)
+ try:
+ return inspect.signature(init, bound_method=True)
+ except ValueError:
+ pass
+
+ # None of the attributes are user-defined, so fall back to let inspect
+ # handle it.
+ # We don't know the exact method that inspect.signature will read
+ # the signature from, so just pass the object itself to our hook.
+ self.env.app.emit('autodoc-before-process-signature', self.object, False)
+ try:
+ return inspect.signature(self.object, bound_method=False)
+ except ValueError:
+ pass
+
+ # Still no signature: happens e.g. for old-style classes
+ # with __init__ in C and no `__text_signature__`.
+ return None
+
def format_args(self, **kwargs: Any) -> str:
if self.env.config.autodoc_typehints in ('none', 'description'):
kwargs.setdefault('show_annotation', False)
- # for classes, the relevant signature is the __init__ method's
- initmeth = self.get_attr(self.object, '__init__', None)
- # classes without __init__ method, default __init__ or
- # __init__ written in C?
- if initmeth is None or \
- inspect.is_builtin_class_method(self.object, '__init__') or \
- not(inspect.ismethod(initmeth) or inspect.isfunction(initmeth)):
- return None
try:
- self.env.app.emit('autodoc-before-process-signature', initmeth, True)
- sig = inspect.signature(initmeth, bound_method=True)
- return stringify_signature(sig, show_return_annotation=False, **kwargs)
- except TypeError:
- # still not possible: happens e.g. for old-style classes
- # with __init__ in C
+ sig = self._get_signature()
+ except TypeError as exc:
+ # __signature__ attribute contained junk
+ logger.warning(__("Failed to get a constructor signature for %s: %s"),
+ self.fullname, exc)
+ return None
+
+ if sig is None:
return None
+ return stringify_signature(sig, show_return_annotation=False, **kwargs)
+
def format_signature(self, **kwargs: Any) -> str:
if self.doc_as_attr:
return ''
@@ -1393,6 +1562,30 @@ class DataDeclarationDocumenter(DataDocumenter):
super().add_content(more_content, no_docstring=True)
+class GenericAliasDocumenter(DataDocumenter):
+ """
+ Specialized Documenter subclass for GenericAliases.
+ """
+
+ objtype = 'genericalias'
+ directivetype = 'data'
+ priority = DataDocumenter.priority + 1
+
+ @classmethod
+ def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
+ ) -> bool:
+ return inspect.isgenericalias(member)
+
+ def add_directive_header(self, sig: str) -> None:
+ self.options.annotation = SUPPRESS # type: ignore
+ super().add_directive_header(sig)
+
+ def add_content(self, more_content: Any, no_docstring: bool = False) -> None:
+ name = stringify_typehint(self.object)
+ content = StringList([_('alias of %s') % name], source='')
+ super().add_content(content)
+
+
class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: ignore
"""
Specialized Documenter subclass for methods (normal, static and class).
@@ -1751,6 +1944,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_autodocumenter(ExceptionDocumenter)
app.add_autodocumenter(DataDocumenter)
app.add_autodocumenter(DataDeclarationDocumenter)
+ app.add_autodocumenter(GenericAliasDocumenter)
app.add_autodocumenter(FunctionDocumenter)
app.add_autodocumenter(DecoratorDocumenter)
app.add_autodocumenter(MethodDocumenter)
@@ -1774,7 +1968,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_event('autodoc-process-signature')
app.add_event('autodoc-skip-member')
- app.connect('config-inited', migrate_autodoc_member_order)
+ app.connect('config-inited', migrate_autodoc_member_order, priority=800)
app.setup_extension('sphinx.ext.autodoc.type_comment')
app.setup_extension('sphinx.ext.autodoc.typehints')