diff options
Diffstat (limited to 'sphinx/ext/autodoc/__init__.py')
-rw-r--r-- | sphinx/ext/autodoc/__init__.py | 1445 |
1 files changed, 1059 insertions, 386 deletions
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index e247d3bd2..83c7d28c4 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -6,40 +6,41 @@ the doctree, thus avoiding duplication between docstrings and documentation for those who like elaborate docstrings. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -import importlib import re import warnings from inspect import Parameter, Signature from types import ModuleType -from typing import ( - Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union -) +from typing import (Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, + TypeVar, Union) from docutils.statemachine import StringList import sphinx from sphinx.application import Sphinx -from sphinx.config import Config, ENUM -from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning +from sphinx.config import ENUM, Config +from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warning, + RemovedInSphinx60Warning) from sphinx.environment import BuildEnvironment -from sphinx.ext.autodoc.importer import import_object, get_module_members, get_object_members -from sphinx.ext.autodoc.mock import mock +from sphinx.ext.autodoc.importer import (get_class_members, get_object_members, import_module, + import_object) +from sphinx.ext.autodoc.mock import ismock, mock from sphinx.locale import _, __ from sphinx.pycode import ModuleAnalyzer, PycodeError -from sphinx.util import inspect -from sphinx.util import logging -from sphinx.util import split_full_qualified_name +from sphinx.util import inspect, logging from sphinx.util.docstrings import extract_metadata, prepare_docstring -from sphinx.util.inspect import getdoc, object_description, safe_getattr, stringify_signature +from sphinx.util.inspect import (evaluate_signature, getdoc, object_description, safe_getattr, + stringify_signature) +from sphinx.util.typing import get_type_hints, restify from sphinx.util.typing import stringify as stringify_typehint if False: # For type annotation from typing import Type # NOQA # for python3.5.1 + from sphinx.ext.autodoc.directive import DocumenterBridge @@ -60,13 +61,29 @@ py_ext_sig_re = re.compile( (?:\s* -> \s* (.*))? # return annotation )? $ # and nothing more ''', re.VERBOSE) +special_member_re = re.compile(r'^__\S+__$') def identity(x: Any) -> Any: return x -ALL = object() +class _All: + """A special value for :*-members: that matches to any member.""" + + def __contains__(self, item: Any) -> bool: + return True + + +class _Empty: + """A special value for :exclude-members: that never matches to any member.""" + + def __contains__(self, item: Any) -> bool: + return False + + +ALL = _All() +EMPTY = _Empty() UNINITIALIZED_ATTR = object() INSTANCEATTR = object() SLOTSATTR = object() @@ -74,21 +91,33 @@ SLOTSATTR = object() def members_option(arg: Any) -> Union[object, List[str]]: """Used to convert the :members: option to auto directives.""" - if arg is None or arg is True: + if arg in (None, True): return ALL - return [x.strip() for x in arg.split(',') if x.strip()] + elif arg is False: + return None + else: + return [x.strip() for x in arg.split(',') if x.strip()] def members_set_option(arg: Any) -> Union[object, Set[str]]: """Used to convert the :members: option to auto directives.""" + warnings.warn("members_set_option() is deprecated.", + RemovedInSphinx50Warning, stacklevel=2) if arg is None: return ALL return {x.strip() for x in arg.split(',') if x.strip()} +def exclude_members_option(arg: Any) -> Union[object, Set[str]]: + """Used to convert the :exclude-members: option.""" + if arg in (None, True): + return EMPTY + return {x.strip() for x in arg.split(',') if x.strip()} + + def inherited_members_option(arg: Any) -> Union[object, Set[str]]: """Used to convert the :members: option to auto directives.""" - if arg is None: + if arg in (None, True): return 'object' else: return arg @@ -96,7 +125,7 @@ def inherited_members_option(arg: Any) -> Union[object, Set[str]]: def member_order_option(arg: Any) -> Optional[str]: """Used to convert the :members: option to auto directives.""" - if arg is None: + if arg in (None, True): return None elif arg in ('alphabetical', 'bysource', 'groupwise'): return arg @@ -108,7 +137,7 @@ SUPPRESS = object() def annotation_option(arg: Any) -> Any: - if arg is None: + if arg in (None, True): # suppress showing the representation of the object return SUPPRESS else: @@ -124,6 +153,8 @@ def bool_option(arg: Any) -> bool: def merge_special_members_option(options: Dict) -> None: """Merge :special-members: option to :members: option.""" + warnings.warn("merge_special_members_option() is deprecated.", + RemovedInSphinx50Warning, stacklevel=2) if 'special-members' in options and options['special-members'] is not ALL: if options.get('members') is ALL: pass @@ -135,6 +166,20 @@ def merge_special_members_option(options: Dict) -> None: options['members'] = options['special-members'] +def merge_members_option(options: Dict) -> None: + """Merge :*-members: option to the :members: option.""" + if options.get('members') is ALL: + # merging is not needed when members: ALL + return + + members = options.setdefault('members', []) + for key in {'private-members', 'special-members'}: + if key in options and options[key] not in (ALL, None): + for member in options[key]: + if member not in members: + members.append(member) + + # Some useful event listener factories for autodoc-process-docstring. def cut_lines(pre: int, post: int = 0, what: str = None) -> Callable: @@ -212,6 +257,35 @@ class Options(dict): return None +class ObjectMember(tuple): + """A member of object. + + This is used for the result of `Documenter.get_object_members()` to + represent each member of the object. + + .. Note:: + + An instance of this class behaves as a tuple of (name, object) + for compatibility to old Sphinx. The behavior will be dropped + in the future. Therefore extensions should not use the tuple + interface. + """ + + def __new__(cls, name: str, obj: Any, **kwargs: Any) -> Any: + return super().__new__(cls, (name, obj)) # type: ignore + + def __init__(self, name: str, obj: Any, docstring: Optional[str] = None, + class_: Any = None, skipped: bool = False) -> None: + self.__name__ = name + self.object = obj + self.docstring = docstring + self.skipped = skipped + self.class_ = class_ + + +ObjectMembers = Union[List[ObjectMember], List[Tuple[str, Any]]] + + class Documenter: """ A Documenter knows how to autodocument a single object type. When @@ -253,6 +327,7 @@ class Documenter: def __init__(self, directive: "DocumenterBridge", name: str, indent: str = '') -> None: self.directive = directive + self.config = directive.env.config self.env = directive.env # type: BuildEnvironment self.options = directive.genopt self.name = name @@ -323,7 +398,7 @@ class Documenter: modname = None parents = [] - with mock(self.env.config.autodoc_mock_imports): + with mock(self.config.autodoc_mock_imports): self.modname, self.objpath = self.resolve_name(modname, parents, path, base) if not self.modname: @@ -335,23 +410,26 @@ class Documenter: ('.' + '.'.join(self.objpath) if self.objpath else '') return True - def import_object(self) -> bool: + def import_object(self, raiseerror: bool = False) -> bool: """Import the object given by *self.modname* and *self.objpath* and set it as *self.object*. Returns True if successful, False if an error occurred. """ - with mock(self.env.config.autodoc_mock_imports): + with mock(self.config.autodoc_mock_imports): try: ret = import_object(self.modname, self.objpath, self.objtype, attrgetter=self.get_attr, - warningiserror=self.env.config.autodoc_warningiserror) + warningiserror=self.config.autodoc_warningiserror) self.module, self.parent, self.object_name, self.object = ret return True except ImportError as exc: - logger.warning(exc.args[0], type='autodoc', subtype='import_object') - self.env.note_reread() - return False + if raiseerror: + raise + else: + logger.warning(exc.args[0], type='autodoc', subtype='import_object') + self.env.note_reread() + return False def get_real_modname(self) -> str: """Get the real module name of an object to document. @@ -422,9 +500,9 @@ class Documenter: 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) + except Exception as exc: + logger.warning(__('error while formatting arguments for %s: %s'), + self.fullname, exc, type='autodoc') args = None result = self.env.events.emit_firstresult('autodoc-process-signature', @@ -460,8 +538,12 @@ class Documenter: # etc. don't support a prepended module name self.add_line(' :module: %s' % self.modname, sourcename) - def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]: - """Decode and return lines of the docstring(s) for the object.""" + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: + """Decode and return lines of the docstring(s) for the object. + + When it returns None value, autodoc-process-docstring will not be called for this + object. + """ if encoding is not None: warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated." % self.__class__.__name__, @@ -470,8 +552,7 @@ class Documenter: warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated." % self.__class__.__name__, RemovedInSphinx50Warning, stacklevel=2) - docstring = getdoc(self.object, self.get_attr, - self.env.config.autodoc_inherit_docstrings, + docstring = getdoc(self.object, self.get_attr, self.config.autodoc_inherit_docstrings, self.parent, self.object_name) if docstring: tab_width = self.directive.state.document.settings.tab_width @@ -486,15 +567,35 @@ class Documenter: self.env.app.emit('autodoc-process-docstring', self.objtype, self.fullname, self.object, self.options, docstringlines) + + if docstringlines and docstringlines[-1] != '': + # append a blank line to the end of the docstring + docstringlines.append('') + yield from docstringlines def get_sourcename(self) -> str: + if (getattr(self.object, '__module__', None) and + getattr(self.object, '__qualname__', None)): + # Get the correct location of docstring from self.object + # to support inherited methods + fullname = '%s.%s' % (self.object.__module__, self.object.__qualname__) + else: + fullname = self.fullname + if self.analyzer: - return '%s:docstring of %s' % (self.analyzer.srcname, self.fullname) - return 'docstring of %s' % self.fullname + return '%s:docstring of %s' % (self.analyzer.srcname, fullname) + else: + return 'docstring of %s' % fullname - def add_content(self, more_content: Any, no_docstring: bool = False) -> None: + def add_content(self, more_content: Optional[StringList], no_docstring: bool = False + ) -> None: """Add content from docstrings, attribute documentation and user.""" + if no_docstring: + warnings.warn("The 'no_docstring' argument to %s.add_content() is deprecated." + % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) + # set sourcename and add content from attribute documentation sourcename = self.get_sourcename() if self.analyzer: @@ -513,33 +614,39 @@ class Documenter: # add content from docstrings if not no_docstring: docstrings = self.get_doc() - if not docstrings: - # append at least a dummy docstring, so that the event - # autodoc-process-docstring is fired and can add some - # content if desired - docstrings.append([]) - for i, line in enumerate(self.process_doc(docstrings)): - self.add_line(line, sourcename, i) + if docstrings is None: + # Do not call autodoc-process-docstring on get_doc() returns None. + pass + else: + if not docstrings: + # append at least a dummy docstring, so that the event + # autodoc-process-docstring is fired and can add some + # content if desired + docstrings.append([]) + for i, line in enumerate(self.process_doc(docstrings)): + self.add_line(line, sourcename, i) # add additional content (e.g. from document), if present if more_content: for line, src in zip(more_content.data, more_content.items): self.add_line(line, src[0], src[1]) - def get_object_members(self, want_all: bool) -> Tuple[bool, List[Tuple[str, Any]]]: + def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]: """Return `(members_check_module, members)` where `members` is a list of `(membername, member)` pairs of the members of *self.object*. If *want_all* is True, return all members. Else, only return those members given by *self.options.members* (which may also be none). """ + warnings.warn('The implementation of Documenter.get_object_members() will be ' + 'removed from Sphinx-6.0.', RemovedInSphinx60Warning) members = get_object_members(self.object, self.objpath, self.get_attr, self.analyzer) if not want_all: if not self.options.members: - return False, [] + return False, [] # type: ignore # specific members given selected = [] - for name in self.options.members: + for name in self.options.members: # type: str if name in members: selected.append((name, members[name].value)) else: @@ -552,7 +659,7 @@ class Documenter: 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 + def filter_members(self, members: ObjectMembers, want_all: bool ) -> List[Tuple[str, Any, bool]]: """Filter the given member list. @@ -567,7 +674,7 @@ class Documenter: The user can override the skipping decision by connecting to the ``autodoc-skip-member`` event. """ - def is_filtered_inherited_member(name: str) -> bool: + def is_filtered_inherited_member(name: str, obj: Any) -> bool: if inspect.isclass(self.object): for cls in self.object.__mro__: if cls.__name__ == self.options.inherited_members and cls != self.object: @@ -575,6 +682,10 @@ class Documenter: return True elif name in cls.__dict__: return False + elif name in self.get_attr(cls, '__annotations__', {}): + return False + elif isinstance(obj, ObjectMember) and obj.class_ is cls: + return False return False @@ -589,14 +700,15 @@ class Documenter: attr_docs = {} # process members and determine which to skip - for (membername, member) in members: + for obj in members: + membername, member = obj # if isattr is True, the member is documented as an attribute if member is INSTANCEATTR: isattr = True else: isattr = False - doc = getdoc(member, self.get_attr, self.env.config.autodoc_inherit_docstrings, + doc = getdoc(member, self.get_attr, self.config.autodoc_inherit_docstrings, self.parent, self.object_name) if not isinstance(doc, str): # Ignore non-string __doc__ @@ -609,6 +721,11 @@ class Documenter: cls_doc = self.get_attr(cls, '__doc__', None) if cls_doc == doc: doc = None + + if isinstance(obj, ObjectMember) and obj.docstring: + # hack for ClassDocumenter to inject docstring via ObjectMember + doc = obj.docstring + has_doc = bool(doc) metadata = extract_metadata(doc) @@ -622,41 +739,55 @@ class Documenter: isprivate = membername.startswith('_') keep = False - if safe_getattr(member, '__sphinx_mock__', False): + if ismock(member): # mocked module or object pass - elif want_all and membername.startswith('__') and \ - membername.endswith('__') and len(membername) > 4: + elif self.options.exclude_members and membername in self.options.exclude_members: + # remove members given by exclude-members + keep = False + elif want_all and special_member_re.match(membername): # special __methods__ - if self.options.special_members is ALL: + if self.options.special_members and membername in self.options.special_members: if membername == '__doc__': keep = False - elif is_filtered_inherited_member(membername): + elif is_filtered_inherited_member(membername, obj): keep = False else: keep = has_doc or self.options.undoc_members - elif self.options.special_members: - if membername in self.options.special_members: - keep = has_doc or self.options.undoc_members + else: + keep = False elif (namespace, membername) in attr_docs: if want_all and isprivate: - # ignore members whose name starts with _ by default - keep = self.options.private_members + if self.options.private_members is None: + keep = False + else: + keep = membername in self.options.private_members else: # keep documented attributes keep = True isattr = True elif want_all and isprivate: - # ignore members whose name starts with _ by default - keep = self.options.private_members and \ - (has_doc or self.options.undoc_members) + if has_doc or self.options.undoc_members: + if self.options.private_members is None: + keep = False + elif is_filtered_inherited_member(membername, obj): + keep = False + else: + keep = membername in self.options.private_members + else: + keep = False else: - if self.options.members is ALL and is_filtered_inherited_member(membername): + if (self.options.members is ALL and + is_filtered_inherited_member(membername, obj)): keep = False else: # ignore undocumented members if :undoc-members: is not given keep = has_doc or self.options.undoc_members + if isinstance(obj, ObjectMember) and obj.skipped: + # forcedly skipped member (ex. a module attribute not defined in __all__) + keep = False + # give the user a chance to decide whether this member # should be skipped if self.env.app: @@ -694,16 +825,6 @@ class Documenter: # find out which members are documentable members_check_module, members = self.get_object_members(want_all) - # remove members given by exclude-members - if self.options.exclude_members: - members = [ - (membername, member) for (membername, member) in members - if ( - self.options.exclude_members is ALL or - membername not in self.options.exclude_members - ) - ] - # document non-skipped members memberdocumenters = [] # type: List[Tuple[Documenter, bool]] for (mname, member, isattr) in self.filter_members(members, want_all): @@ -721,7 +842,7 @@ class Documenter: 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 + member_order = self.options.member_order or self.config.autodoc_member_order memberdocumenters = self.sort_members(memberdocumenters, member_order) for documenter, isattr in memberdocumenters: @@ -758,7 +879,7 @@ class Documenter: return documenters - def generate(self, more_content: Any = None, real_modname: str = None, + def generate(self, more_content: Optional[StringList] = 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 its members. @@ -795,8 +916,8 @@ class Documenter: # parse right now, to get PycodeErrors on parsing (results will # be cached anyway) self.analyzer.find_attr_docs() - except PycodeError: - logger.debug('[autodoc] module analyzer failed:', exc_info=True) + except PycodeError as exc: + logger.debug('[autodoc] module analyzer failed: %s', exc) # no source file -- e.g. for builtin and C modules self.analyzer = None # at least add the module.__file__ as a dependency @@ -826,7 +947,12 @@ class Documenter: self.add_line('', sourcename) # format the object's signature, if any - sig = self.format_signature() + try: + sig = self.format_signature() + except Exception as exc: + logger.warning(__('error while formatting signature for %s: %s'), + self.fullname, exc, type='autodoc') + return # generate the directive header and options, if applicable self.add_directive_header(sig) @@ -855,15 +981,15 @@ class ModuleDocumenter(Documenter): 'noindex': bool_option, 'inherited-members': inherited_members_option, 'show-inheritance': bool_option, 'synopsis': identity, 'platform': identity, 'deprecated': bool_option, - 'member-order': member_order_option, 'exclude-members': members_set_option, - 'private-members': bool_option, 'special-members': members_option, + 'member-order': member_order_option, 'exclude-members': exclude_members_option, + 'private-members': members_option, 'special-members': members_option, 'imported-members': bool_option, 'ignore-module-all': bool_option } # type: Dict[str, Callable] def __init__(self, *args: Any) -> None: super().__init__(*args) - merge_special_members_option(self.options) - self.__all__ = None + merge_members_option(self.options) + self.__all__ = None # type: Optional[Sequence[str]] @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any @@ -886,27 +1012,21 @@ 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() + def import_object(self, raiseerror: bool = False) -> bool: + ret = super().import_object(raiseerror) - 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') + try: + if not self.options.ignore_module_all: + self.__all__ = inspect.getall(self.object) + except AttributeError as exc: + # __all__ raises an error. + logger.warning(__('%s.__all__ raises an error. Ignored: %r'), + (self.fullname, exc), type='autodoc') + except ValueError as exc: + # invalid __all__ found. + logger.warning(__('__all__ should be a list of strings, not %r ' + '(in module %s) -- ignoring __all__') % + (exc.args[0], self.fullname), type='autodoc') return ret @@ -923,28 +1043,59 @@ class ModuleDocumenter(Documenter): if self.options.deprecated: self.add_line(' :deprecated:', sourcename) - def get_object_members(self, want_all: bool) -> Tuple[bool, List[Tuple[str, Any]]]: + def get_module_members(self) -> Dict[str, ObjectMember]: + """Get members of target module.""" + if self.analyzer: + attr_docs = self.analyzer.attr_docs + else: + attr_docs = {} + + members = {} # type: Dict[str, ObjectMember] + for name in dir(self.object): + try: + value = safe_getattr(self.object, name, None) + docstring = attr_docs.get(('', name), []) + members[name] = ObjectMember(name, value, docstring="\n".join(docstring)) + except AttributeError: + continue + + # annotation only member (ex. attr: int) + try: + for name in inspect.getannotations(self.object): + if name not in members: + docstring = attr_docs.get(('', name), []) + members[name] = ObjectMember(name, INSTANCEATTR, + docstring="\n".join(docstring)) + except AttributeError: + pass + + return members + + def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]: + members = self.get_module_members() if want_all: - if self.__all__: - memberlist = self.__all__ - else: + if self.__all__ is None: # for implicit module members, check __module__ to avoid # documenting imported objects - return True, get_module_members(self.object) + return True, list(members.values()) + else: + for member in members.values(): + if member.__name__ not in self.__all__: + member.skipped = True + + return False, list(members.values()) else: memberlist = self.options.members or [] - ret = [] - for mname in memberlist: - try: - ret.append((mname, safe_getattr(self.object, mname))) - except AttributeError: - logger.warning( - __('missing attribute mentioned in :members: or __all__: ' - 'module %s, attribute %s') % - (safe_getattr(self.object, '__name__', '???'), mname), - type='autodoc' - ) - return False, ret + ret = [] + for name in memberlist: + if name in members: + ret.append(members[name]) + else: + logger.warning(__('missing attribute mentioned in :members: option: ' + 'module %s, attribute %s') % + (safe_getattr(self.object, '__name__', '???'), name), + type='autodoc') + return False, ret def sort_members(self, documenters: List[Tuple["Documenter", bool]], order: str) -> List[Tuple["Documenter", bool]]: @@ -975,14 +1126,8 @@ class ModuleLevelDocumenter(Documenter): ) -> Tuple[str, List[str]]: if modname is None: if path: - stripped = path.rstrip('.') - modname, qualname = split_full_qualified_name(stripped) - if qualname: - parents = qualname.split(".") - else: - parents = [] - - if modname is None: + modname = path.rstrip('.') + else: # if documenting a toplevel object without explicit module, # it can be contained in another auto directive ... modname = self.env.temp_data.get('autodoc:module') @@ -1015,13 +1160,8 @@ class ClassLevelDocumenter(Documenter): # ... if still None, there's no way to know if mod_cls is None: return None, [] - - try: - modname, qualname = split_full_qualified_name(mod_cls) - parents = qualname.split(".") if qualname else [] - except ImportError: - parents = mod_cls.split(".") - + modname, sep, cls = mod_cls.rpartition('.') + parents = [cls] # if the module name is still missing, get it like above if not modname: modname = self.env.temp_data.get('autodoc:module') @@ -1053,6 +1193,8 @@ class DocstringSignatureMixin: valid_names.extend(cls.__name__ for cls in self.object.__mro__) docstrings = self.get_doc() + if docstrings is None: + return None, None self._new_docstrings = docstrings[:] self._signatures = [] result = None @@ -1103,7 +1245,7 @@ class DocstringSignatureMixin: return result - def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]: + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: if encoding is not None: warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated." % self.__class__.__name__, @@ -1113,7 +1255,7 @@ class DocstringSignatureMixin: return super().get_doc(None, ignore) # type: ignore def format_signature(self, **kwargs: Any) -> str: - if self.args is None and self.env.config.autodoc_docstring_signature: # type: ignore + if self.args is None and self.config.autodoc_docstring_signature: # type: ignore # only act if a signature is not explicitly given already, and if # the feature is enabled result = self._find_signature() @@ -1132,7 +1274,7 @@ class DocstringStripSignatureMixin(DocstringSignatureMixin): feature of stripping any function signature from the docstring. """ def format_signature(self, **kwargs: Any) -> str: - if self.args is None and self.env.config.autodoc_docstring_signature: # type: ignore + if self.args is None and self.config.autodoc_docstring_signature: # type: ignore # only act if a signature is not explicitly given already, and if # the feature is enabled result = self._find_signature() @@ -1159,15 +1301,12 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ (inspect.isroutine(member) and isinstance(parent, ModuleDocumenter))) def format_args(self, **kwargs: Any) -> str: - if self.env.config.autodoc_typehints in ('none', 'description'): + if self.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) try: self.env.app.emit('autodoc-before-process-signature', self.object, False) - if inspect.is_singledispatch_function(self.object): - sig = inspect.signature(self.object, follow_wrapped=True) - else: - sig = inspect.signature(self.object) + sig = inspect.signature(self.object, type_aliases=self.config.autodoc_type_aliases) args = stringify_signature(sig, **kwargs) except TypeError as exc: logger.warning(__("Failed to get a function signature for %s: %s"), @@ -1176,7 +1315,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ except ValueError: args = '' - if self.env.config.strip_signature_backslash: + if self.config.strip_signature_backslash: # escape backslashes for reST args = args.replace('\\', '\\\\') return args @@ -1192,8 +1331,16 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ self.add_line(' :async:', sourcename) def format_signature(self, **kwargs: Any) -> str: - sig = super().format_signature(**kwargs) - sigs = [sig] + sigs = [] + if (self.analyzer and + '.'.join(self.objpath) in self.analyzer.overloads and + self.config.autodoc_typehints == 'signature'): + # Use signatures for overloaded functions instead of the implementation function. + overloaded = True + else: + overloaded = False + sig = super().format_signature(**kwargs) + sigs.append(sig) if inspect.is_singledispatch_function(self.object): # append signature of singledispatch'ed functions @@ -1207,27 +1354,52 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ documenter.object = func documenter.objpath = [None] sigs.append(documenter.format_signature()) + if overloaded: + actual = inspect.signature(self.object, + type_aliases=self.config.autodoc_type_aliases) + __globals__ = safe_getattr(self.object, '__globals__', {}) + for overload in self.analyzer.overloads.get('.'.join(self.objpath)): + overload = self.merge_default_value(actual, overload) + overload = evaluate_signature(overload, __globals__, + self.config.autodoc_type_aliases) + + sig = stringify_signature(overload, **kwargs) + sigs.append(sig) return "\n".join(sigs) + def merge_default_value(self, actual: Signature, overload: Signature) -> Signature: + """Merge default values of actual implementation to the overload variants.""" + parameters = list(overload.parameters.values()) + for i, param in enumerate(parameters): + actual_param = actual.parameters.get(param.name) + if actual_param and param.default == '...': + parameters[i] = param.replace(default=actual_param.default) + + return overload.replace(parameters=parameters) + def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: """Annotate type hint to the first argument of function if needed.""" - sig = inspect.signature(func) + try: + sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) + except TypeError as exc: + logger.warning(__("Failed to get a function signature for %s: %s"), + self.fullname, exc) + return + except ValueError: + return + if len(sig.parameters) == 0: return params = list(sig.parameters.values()) if params[0].annotation is Parameter.empty: params[0] = params[0].replace(annotation=typ) - func.__signature__ = sig.replace(parameters=params) # type: ignore - - -class SingledispatchFunctionDocumenter(FunctionDocumenter): - """ - Used to be a specialized Documenter subclass for singledispatch'ed functions. - - Retained for backwards compatibility, now does the same as the FunctionDocumenter - """ + try: + func.__signature__ = sig.replace(parameters=params) # type: ignore + except TypeError: + # failed to update signature (ex. built-in or extension types) + return class DecoratorDocumenter(FunctionDocumenter): @@ -1255,6 +1427,12 @@ _METACLASS_CALL_BLACKLIST = [ ] +# Types whose __new__ signature is a pass-thru. +_CLASS_NEW_BLACKLIST = [ + 'typing.Generic.__new__', +] + + class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: ignore """ Specialized Documenter subclass for classes. @@ -1265,21 +1443,24 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: 'members': members_option, 'undoc-members': bool_option, 'noindex': bool_option, 'inherited-members': inherited_members_option, 'show-inheritance': bool_option, 'member-order': member_order_option, - 'exclude-members': members_set_option, - 'private-members': bool_option, 'special-members': members_option, + 'exclude-members': exclude_members_option, + 'private-members': members_option, 'special-members': members_option, } # type: Dict[str, Callable] + _signature_class = None # type: Any + _signature_method_name = None # type: str + def __init__(self, *args: Any) -> None: super().__init__(*args) - merge_special_members_option(self.options) + merge_members_option(self.options) @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any ) -> bool: return isinstance(member, type) - def import_object(self) -> Any: - ret = super().import_object() + def import_object(self, raiseerror: bool = False) -> bool: + ret = super().import_object(raiseerror) # if the class is documented under another name, document it # as data/attribute if ret: @@ -1289,7 +1470,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: self.doc_as_attr = True return ret - def _get_signature(self) -> Optional[Signature]: + def _get_signature(self) -> Tuple[Optional[Any], Optional[str], 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): @@ -1302,7 +1483,12 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: # 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 + # First, we check the obj has a __signature__ attribute + if (hasattr(self.object, '__signature__') and + isinstance(self.object.__signature__, Signature)): + return None, None, self.object.__signature__ + + # Next, let's see if it has an overloaded __call__ defined # in its metaclass call = get_user_defined_function_or_method(type(self.object), '__call__') @@ -1313,16 +1499,25 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: if call is not None: self.env.app.emit('autodoc-before-process-signature', call, True) try: - return inspect.signature(call, bound_method=True) + sig = inspect.signature(call, bound_method=True, + type_aliases=self.config.autodoc_type_aliases) + return type(self.object), '__call__', sig 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: + if "{0.__module__}.{0.__qualname__}".format(new) in _CLASS_NEW_BLACKLIST: + new = None + if new is not None: self.env.app.emit('autodoc-before-process-signature', new, True) try: - return inspect.signature(new, bound_method=True) + sig = inspect.signature(new, bound_method=True, + type_aliases=self.config.autodoc_type_aliases) + return self.object, '__new__', sig except ValueError: pass @@ -1331,7 +1526,9 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: if init is not None: self.env.app.emit('autodoc-before-process-signature', init, True) try: - return inspect.signature(init, bound_method=True) + sig = inspect.signature(init, bound_method=True, + type_aliases=self.config.autodoc_type_aliases) + return self.object, '__init__', sig except ValueError: pass @@ -1341,20 +1538,22 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: # 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) + sig = inspect.signature(self.object, bound_method=False, + type_aliases=self.config.autodoc_type_aliases) + return None, None, sig except ValueError: pass # Still no signature: happens e.g. for old-style classes # with __init__ in C and no `__text_signature__`. - return None + return None, None, None def format_args(self, **kwargs: Any) -> str: - if self.env.config.autodoc_typehints in ('none', 'description'): + if self.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) try: - sig = self._get_signature() + self._signature_class, self._signature_method_name, sig = self._get_signature() except TypeError as exc: # __signature__ attribute contained junk logger.warning(__("Failed to get a constructor signature for %s: %s"), @@ -1370,7 +1569,44 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: if self.doc_as_attr: return '' - return super().format_signature(**kwargs) + sig = super().format_signature() + sigs = [] + + overloads = self.get_overloaded_signatures() + if overloads and self.config.autodoc_typehints == 'signature': + # Use signatures for overloaded methods instead of the implementation method. + method = safe_getattr(self._signature_class, self._signature_method_name, None) + __globals__ = safe_getattr(method, '__globals__', {}) + for overload in overloads: + overload = evaluate_signature(overload, __globals__, + self.config.autodoc_type_aliases) + + parameters = list(overload.parameters.values()) + overload = overload.replace(parameters=parameters[1:], + return_annotation=Parameter.empty) + sig = stringify_signature(overload, **kwargs) + sigs.append(sig) + else: + sigs.append(sig) + + return "\n".join(sigs) + + def get_overloaded_signatures(self) -> List[Signature]: + if self._signature_class and self._signature_method_name: + for cls in self._signature_class.__mro__: + try: + analyzer = ModuleAnalyzer.for_module(cls.__module__) + analyzer.analyze() + qualname = '.'.join([cls.__qualname__, self._signature_method_name]) + if qualname in analyzer.overloads: + return analyzer.overloads.get(qualname) + elif qualname in analyzer.tagorder: + # the constructor is defined in the class, but not overrided. + return [] + except PycodeError: + pass + + return [] def add_directive_header(self, sig: str) -> None: sourcename = self.get_sourcename() @@ -1386,24 +1622,50 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: if not self.doc_as_attr and self.options.show_inheritance: sourcename = self.get_sourcename() self.add_line('', sourcename) - if hasattr(self.object, '__bases__') and len(self.object.__bases__): - bases = [':class:`%s`' % b.__name__ - if b.__module__ in ('__builtin__', 'builtins') - else ':class:`%s.%s`' % (b.__module__, b.__qualname__) - for b in self.object.__bases__] - self.add_line(' ' + _('Bases: %s') % ', '.join(bases), - sourcename) - def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]: + if hasattr(self.object, '__orig_bases__') and len(self.object.__orig_bases__): + # A subclass of generic types + # refs: PEP-560 <https://www.python.org/dev/peps/pep-0560/> + bases = [restify(cls) for cls in self.object.__orig_bases__] + self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename) + elif hasattr(self.object, '__bases__') and len(self.object.__bases__): + # A normal class + bases = [restify(cls) for cls in self.object.__bases__] + self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename) + + def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]: + members = get_class_members(self.object, self.objpath, self.get_attr) + if not want_all: + if not self.options.members: + return False, [] # type: ignore + # specific members given + selected = [] + for name in self.options.members: # type: str + if name in members: + selected.append(members[name]) + else: + logger.warning(__('missing attribute %s in object %s') % + (name, self.fullname), type='autodoc') + return False, selected + elif self.options.inherited_members: + return False, list(members.values()) + else: + return False, [m for m in members.values() if m.class_ == self.object] + + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: if encoding is not None: warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated." % self.__class__.__name__, RemovedInSphinx40Warning, stacklevel=2) + if self.doc_as_attr: + # Don't show the docstring of the class when it is an alias. + return None + lines = getattr(self, '_new_docstrings', None) if lines is not None: return lines - content = self.env.config.autoclass_content + content = self.config.autoclass_content docstrings = [] attrdocstring = self.get_attr(self.object, '__doc__', None) @@ -1415,7 +1677,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: if content in ('both', 'init'): __init__ = self.get_attr(self.object, '__init__', None) initdocstring = getdoc(__init__, self.get_attr, - self.env.config.autodoc_inherit_docstrings, + self.config.autodoc_inherit_docstrings, self.parent, self.object_name) # for new-style classes, no __init__ means default __init__ if (initdocstring is not None and @@ -1426,7 +1688,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: # try __new__ __new__ = self.get_attr(self.object, '__new__', None) initdocstring = getdoc(__new__, self.get_attr, - self.env.config.autodoc_inherit_docstrings, + self.config.autodoc_inherit_docstrings, self.parent, self.object_name) # for new-style classes, no __new__ means default __new__ if (initdocstring is not None and @@ -1442,27 +1704,22 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: tab_width = self.directive.state.document.settings.tab_width return [prepare_docstring(docstring, ignore, tab_width) for docstring in docstrings] - def add_content(self, more_content: Any, no_docstring: bool = False) -> None: + def add_content(self, more_content: Optional[StringList], no_docstring: bool = False + ) -> None: if self.doc_as_attr: - classname = safe_getattr(self.object, '__qualname__', None) - if not classname: - classname = safe_getattr(self.object, '__name__', None) - if classname: - module = safe_getattr(self.object, '__module__', None) - parentmodule = safe_getattr(self.parent, '__module__', None) - if module and module != parentmodule: - classname = str(module) + '.' + str(classname) - content = StringList([_('alias of :class:`%s`') % classname], source='') - super().add_content(content, no_docstring=True) - else: - super().add_content(more_content) + try: + more_content = StringList([_('alias of %s') % restify(self.object)], source='') + except AttributeError: + pass # Invalid class object is passed. + + super().add_content(more_content) def document_members(self, all_members: bool = False) -> None: if self.doc_as_attr: return super().document_members(all_members) - def generate(self, more_content: Any = None, real_modname: str = None, + def generate(self, more_content: Optional[StringList] = None, real_modname: str = None, check_module: bool = False, all_members: bool = False) -> None: # Do not pass real_modname and use the name from the __module__ # attribute of the class. @@ -1490,7 +1747,149 @@ class ExceptionDocumenter(ClassDocumenter): return isinstance(member, type) and issubclass(member, BaseException) -class DataDocumenter(ModuleLevelDocumenter): +class DataDocumenterMixinBase: + # define types of instance variables + config = None # type: Config + env = None # type: BuildEnvironment + modname = None # type: str + parent = None # type: Any + object = None # type: Any + objpath = None # type: List[str] + + def should_suppress_directive_header(self) -> bool: + """Check directive header should be suppressed.""" + return False + + def should_suppress_value_header(self) -> bool: + """Check :value: header should be suppressed.""" + return False + + def update_content(self, more_content: StringList) -> None: + """Update docstring for the NewType object.""" + pass + + +class GenericAliasMixin(DataDocumenterMixinBase): + """ + Mixin for DataDocumenter and AttributeDocumenter to provide the feature for + supporting GenericAliases. + """ + + def should_suppress_directive_header(self) -> bool: + return (inspect.isgenericalias(self.object) or + super().should_suppress_directive_header()) + + def update_content(self, more_content: StringList) -> None: + if inspect.isgenericalias(self.object): + alias = stringify_typehint(self.object) + more_content.append(_('alias of %s') % alias, '') + more_content.append('', '') + + super().update_content(more_content) + + +class NewTypeMixin(DataDocumenterMixinBase): + """ + Mixin for DataDocumenter and AttributeDocumenter to provide the feature for + supporting NewTypes. + """ + + def should_suppress_directive_header(self) -> bool: + return (inspect.isNewType(self.object) or + super().should_suppress_directive_header()) + + def update_content(self, more_content: StringList) -> None: + if inspect.isNewType(self.object): + supertype = restify(self.object.__supertype__) + more_content.append(_('alias of %s') % supertype, '') + more_content.append('', '') + + super().update_content(more_content) + + +class TypeVarMixin(DataDocumenterMixinBase): + """ + Mixin for DataDocumenter and AttributeDocumenter to provide the feature for + supporting TypeVars. + """ + + def should_suppress_directive_header(self) -> bool: + return (isinstance(self.object, TypeVar) or + super().should_suppress_directive_header()) + + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: + if ignore is not None: + warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated." + % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) + + if isinstance(self.object, TypeVar): + if self.object.__doc__ != TypeVar.__doc__: + return super().get_doc() # type: ignore + else: + return [] + else: + return super().get_doc() # type: ignore + + def update_content(self, more_content: StringList) -> None: + if isinstance(self.object, TypeVar): + attrs = [repr(self.object.__name__)] + for constraint in self.object.__constraints__: + attrs.append(stringify_typehint(constraint)) + if self.object.__covariant__: + attrs.append("covariant=True") + if self.object.__contravariant__: + attrs.append("contravariant=True") + + more_content.append(_('alias of TypeVar(%s)') % ", ".join(attrs), '') + more_content.append('', '') + + super().update_content(more_content) + + +class UninitializedGlobalVariableMixin(DataDocumenterMixinBase): + """ + Mixin for DataDocumenter to provide the feature for supporting uninitialized + (type annotation only) global variables. + """ + + def import_object(self, raiseerror: bool = False) -> bool: + try: + return super().import_object(raiseerror=True) # type: ignore + except ImportError as exc: + # annotation only instance variable (PEP-526) + try: + with mock(self.config.autodoc_mock_imports): + parent = import_module(self.modname, self.config.autodoc_warningiserror) + annotations = get_type_hints(parent, None, + self.config.autodoc_type_aliases) + if self.objpath[-1] in annotations: + self.object = UNINITIALIZED_ATTR + self.parent = parent + return True + except ImportError: + pass + + if raiseerror: + raise + else: + logger.warning(exc.args[0], type='autodoc', subtype='import_object') + self.env.note_reread() + return False + + def should_suppress_value_header(self) -> bool: + return (self.object is UNINITIALIZED_ATTR or + super().should_suppress_value_header()) + + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: + if self.object is UNINITIALIZED_ATTR: + return [] + else: + return super().get_doc(encoding, ignore) # type: ignore + + +class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin, + UninitializedGlobalVariableMixin, ModuleLevelDocumenter): """ Specialized Documenter subclass for data items. """ @@ -1499,40 +1898,68 @@ class DataDocumenter(ModuleLevelDocumenter): priority = -10 option_spec = dict(ModuleLevelDocumenter.option_spec) option_spec["annotation"] = annotation_option + option_spec["no-value"] = bool_option @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any ) -> bool: return isinstance(parent, ModuleDocumenter) and isattr + def update_annotations(self, parent: Any) -> None: + """Update __annotations__ to support type_comment and so on.""" + try: + annotations = dict(inspect.getannotations(parent)) + parent.__annotations__ = annotations + + analyzer = ModuleAnalyzer.for_module(self.modname) + analyzer.analyze() + for (classname, attrname), annotation in analyzer.annotations.items(): + if classname == '' and attrname not in annotations: + annotations[attrname] = annotation + except AttributeError: + pass + + def import_object(self, raiseerror: bool = False) -> bool: + ret = super().import_object(raiseerror) + if self.parent: + self.update_annotations(self.parent) + + return ret + + def should_suppress_value_header(self) -> bool: + if super().should_suppress_value_header(): + return True + else: + doc = self.get_doc() + metadata = extract_metadata('\n'.join(sum(doc, []))) + if 'hide-value' in metadata: + return True + + return False + def add_directive_header(self, sig: str) -> None: super().add_directive_header(sig) sourcename = self.get_sourcename() - if not self.options.annotation: + if self.options.annotation is SUPPRESS or self.should_suppress_directive_header(): + pass + elif self.options.annotation: + self.add_line(' :annotation: %s' % self.options.annotation, + sourcename) + else: # obtain annotation for this data - annotations = getattr(self.parent, '__annotations__', {}) - if annotations and self.objpath[-1] in annotations: + annotations = get_type_hints(self.parent, None, self.config.autodoc_type_aliases) + if self.objpath[-1] in annotations: objrepr = stringify_typehint(annotations.get(self.objpath[-1])) self.add_line(' :type: ' + objrepr, sourcename) - else: - key = ('.'.join(self.objpath[:-1]), self.objpath[-1]) - if self.analyzer and key in self.analyzer.annotations: - self.add_line(' :type: ' + self.analyzer.annotations[key], - sourcename) try: - if self.object is UNINITIALIZED_ATTR: + if self.options.no_value or self.should_suppress_value_header(): pass else: objrepr = object_description(self.object) self.add_line(' :value: ' + objrepr, sourcename) except ValueError: pass - elif self.options.annotation is SUPPRESS: - pass - else: - self.add_line(' :annotation: %s' % self.options.annotation, - sourcename) def document_members(self, all_members: bool = False) -> None: pass @@ -1541,67 +1968,55 @@ class DataDocumenter(ModuleLevelDocumenter): return self.get_attr(self.parent or self.object, '__module__', None) \ or self.modname + def get_module_comment(self, attrname: str) -> Optional[List[str]]: + try: + analyzer = ModuleAnalyzer.for_module(self.modname) + analyzer.analyze() + key = ('', attrname) + if key in analyzer.attr_docs: + return list(analyzer.attr_docs[key]) + except PycodeError: + pass -class DataDeclarationDocumenter(DataDocumenter): - """ - Specialized Documenter subclass for data that cannot be imported - because they are declared without initial value (refs: PEP-526). - """ - objtype = 'datadecl' - directivetype = 'data' - member_order = 60 + return None - # must be higher than AttributeDocumenter - priority = 11 + def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]: + # Check the variable has a docstring-comment + comment = self.get_module_comment(self.objpath[-1]) + if comment: + return [comment] + else: + return super().get_doc(encoding, ignore) - @classmethod - def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any - ) -> bool: - """This documents only INSTANCEATTR members.""" - return (isinstance(parent, ModuleDocumenter) and - isattr and - member is INSTANCEATTR) - - def import_object(self) -> bool: - """Never import anything.""" - # disguise as a data - self.objtype = 'data' - self.object = UNINITIALIZED_ATTR - try: - # import module to obtain type annotation - self.parent = importlib.import_module(self.modname) - except ImportError: - pass + def add_content(self, more_content: Optional[StringList], no_docstring: bool = False + ) -> None: + # Disable analyzing variable comment on Documenter.add_content() to control it on + # DataDocumenter.add_content() + self.analyzer = None - return True + if not more_content: + more_content = StringList() - def add_content(self, more_content: Any, no_docstring: bool = False) -> None: - """Never try to get a docstring from the object.""" - super().add_content(more_content, no_docstring=True) + self.update_content(more_content) + super().add_content(more_content, no_docstring=no_docstring) -class GenericAliasDocumenter(DataDocumenter): +class NewTypeDataDocumenter(DataDocumenter): """ - Specialized Documenter subclass for GenericAliases. + Specialized Documenter subclass for NewTypes. + + Note: This must be invoked before FunctionDocumenter because NewType is a kind of + function object. """ - objtype = 'genericalias' + objtype = 'newtypedata' directivetype = 'data' - priority = DataDocumenter.priority + 1 + priority = FunctionDocumenter.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) + return inspect.isNewType(member) and isattr class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: ignore @@ -1619,8 +2034,8 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: return inspect.isroutine(member) and \ not isinstance(parent, ModuleDocumenter) - def import_object(self) -> Any: - ret = super().import_object() + def import_object(self, raiseerror: bool = False) -> bool: + ret = super().import_object(raiseerror) if not ret: return ret @@ -1637,7 +2052,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: return ret def format_args(self, **kwargs: Any) -> str: - if self.env.config.autodoc_typehints in ('none', 'description'): + if self.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) try: @@ -1650,16 +2065,12 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: else: if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): self.env.app.emit('autodoc-before-process-signature', self.object, False) - sig = inspect.signature(self.object, bound_method=False) + sig = inspect.signature(self.object, bound_method=False, + type_aliases=self.config.autodoc_type_aliases) else: self.env.app.emit('autodoc-before-process-signature', self.object, True) - - meth = self.parent.__dict__.get(self.objpath[-1], None) - if meth and inspect.is_singledispatch_method(meth): - sig = inspect.signature(self.object, bound_method=True, - follow_wrapped=True) - else: - sig = inspect.signature(self.object, bound_method=True) + sig = inspect.signature(self.object, bound_method=True, + type_aliases=self.config.autodoc_type_aliases) args = stringify_signature(sig, **kwargs) except TypeError as exc: logger.warning(__("Failed to get a method signature for %s: %s"), @@ -1668,7 +2079,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: except ValueError: args = '' - if self.env.config.strip_signature_backslash: + if self.config.strip_signature_backslash: # escape backslashes for reST args = args.replace('\\', '\\\\') return args @@ -1693,8 +2104,16 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: pass def format_signature(self, **kwargs: Any) -> str: - sig = super().format_signature(**kwargs) - sigs = [sig] + sigs = [] + if (self.analyzer and + '.'.join(self.objpath) in self.analyzer.overloads and + self.config.autodoc_typehints == 'signature'): + # Use signatures for overloaded methods instead of the implementation method. + overloaded = True + else: + overloaded = False + sig = super().format_signature(**kwargs) + sigs.append(sig) meth = self.parent.__dict__.get(self.objpath[-1]) if inspect.is_singledispatch_method(meth): @@ -1710,30 +2129,252 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: documenter.object = func documenter.objpath = [None] sigs.append(documenter.format_signature()) + if overloaded: + if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): + actual = inspect.signature(self.object, bound_method=False, + type_aliases=self.config.autodoc_type_aliases) + else: + actual = inspect.signature(self.object, bound_method=True, + type_aliases=self.config.autodoc_type_aliases) + + __globals__ = safe_getattr(self.object, '__globals__', {}) + for overload in self.analyzer.overloads.get('.'.join(self.objpath)): + overload = self.merge_default_value(actual, overload) + overload = evaluate_signature(overload, __globals__, + self.config.autodoc_type_aliases) + + if not inspect.isstaticmethod(self.object, cls=self.parent, + name=self.object_name): + parameters = list(overload.parameters.values()) + overload = overload.replace(parameters=parameters[1:]) + sig = stringify_signature(overload, **kwargs) + sigs.append(sig) return "\n".join(sigs) + def merge_default_value(self, actual: Signature, overload: Signature) -> Signature: + """Merge default values of actual implementation to the overload variants.""" + parameters = list(overload.parameters.values()) + for i, param in enumerate(parameters): + actual_param = actual.parameters.get(param.name) + if actual_param and param.default == '...': + parameters[i] = param.replace(default=actual_param.default) + + return overload.replace(parameters=parameters) + def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: """Annotate type hint to the first argument of function if needed.""" - sig = inspect.signature(func) + try: + sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) + except TypeError as exc: + logger.warning(__("Failed to get a method signature for %s: %s"), + self.fullname, exc) + return + except ValueError: + return if len(sig.parameters) == 1: return params = list(sig.parameters.values()) if params[1].annotation is Parameter.empty: params[1] = params[1].replace(annotation=typ) - func.__signature__ = sig.replace(parameters=params) # type: ignore + try: + func.__signature__ = sig.replace(parameters=params) # type: ignore + except TypeError: + # failed to update signature (ex. built-in or extension types) + return + + +class NonDataDescriptorMixin(DataDocumenterMixinBase): + """ + Mixin for AttributeDocumenter to provide the feature for supporting non + data-descriptors. + + .. note:: This mix-in must be inherited after other mix-ins. Otherwise, docstring + and :value: header will be suppressed unexpectedly. + """ + + def import_object(self, raiseerror: bool = False) -> bool: + ret = super().import_object(raiseerror) # type: ignore + if ret and not inspect.isattributedescriptor(self.object): + self.non_data_descriptor = True + else: + self.non_data_descriptor = False + + return ret + + def should_suppress_value_header(self) -> bool: + return (not getattr(self, 'non_data_descriptor', False) or + super().should_suppress_directive_header()) + + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: + if getattr(self, 'non_data_descriptor', False): + # the docstring of non datadescriptor is very probably the wrong thing + # to display + return None + else: + return super().get_doc(encoding, ignore) # type: ignore + + +class SlotsMixin(DataDocumenterMixinBase): + """ + Mixin for AttributeDocumenter to provide the feature for supporting __slots__. + """ + + def isslotsattribute(self) -> bool: + """Check the subject is an attribute in __slots__.""" + try: + __slots__ = inspect.getslots(self.parent) + if __slots__ and self.objpath[-1] in __slots__: + return True + else: + return False + except (AttributeError, ValueError, TypeError): + return False + + def import_object(self, raiseerror: bool = False) -> bool: + ret = super().import_object(raiseerror) # type: ignore + if self.isslotsattribute(): + self.object = SLOTSATTR + + return ret + + def should_suppress_directive_header(self) -> bool: + if self.object is SLOTSATTR: + self._datadescriptor = True + return True + else: + return super().should_suppress_directive_header() + + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: + if self.object is SLOTSATTR: + try: + __slots__ = inspect.getslots(self.parent) + if __slots__ and __slots__.get(self.objpath[-1]): + docstring = prepare_docstring(__slots__[self.objpath[-1]]) + return [docstring] + else: + return [] + except (AttributeError, ValueError) as exc: + logger.warning(__('Invalid __slots__ found on %s. Ignored.'), + (self.parent.__qualname__, exc), type='autodoc') + return [] + else: + return super().get_doc(encoding, ignore) # type: ignore + + +class RuntimeInstanceAttributeMixin(DataDocumenterMixinBase): + """ + Mixin for AttributeDocumenter to provide the feature for supporting runtime + instance attributes (that are defined in __init__() methods with doc-comments). + + Example: + + class Foo: + def __init__(self): + self.attr = None #: This is a target of this mix-in. + """ + + RUNTIME_INSTANCE_ATTRIBUTE = object() + + def is_runtime_instance_attribute(self, parent: Any) -> bool: + """Check the subject is an attribute defined in __init__().""" + # An instance variable defined in __init__(). + if self.get_attribute_comment(parent, self.objpath[-1]): # type: ignore + return True + else: + return False + + def import_object(self, raiseerror: bool = False) -> bool: + """Check the existence of runtime instance attribute when failed to import the + attribute.""" + try: + return super().import_object(raiseerror=True) # type: ignore + except ImportError as exc: + try: + with mock(self.config.autodoc_mock_imports): + ret = import_object(self.modname, self.objpath[:-1], 'class', + attrgetter=self.get_attr, # type: ignore + warningiserror=self.config.autodoc_warningiserror) + parent = ret[3] + if self.is_runtime_instance_attribute(parent): + self.object = self.RUNTIME_INSTANCE_ATTRIBUTE + self.parent = parent + return True + except ImportError: + pass + + if raiseerror: + raise + else: + logger.warning(exc.args[0], type='autodoc', subtype='import_object') + self.env.note_reread() + return False + def should_suppress_value_header(self) -> bool: + return (self.object is self.RUNTIME_INSTANCE_ATTRIBUTE or + super().should_suppress_value_header()) -class SingledispatchMethodDocumenter(MethodDocumenter): + +class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase): """ - Used to be a specialized Documenter subclass for singledispatch'ed methods. + Mixin for AttributeDocumenter to provide the feature for supporting uninitialized + instance attributes (PEP-526 styled, annotation only attributes). + + Example: - Retained for backwards compatibility, now does the same as the MethodDocumenter + class Foo: + attr: int #: This is a target of this mix-in. """ + def is_uninitialized_instance_attribute(self, parent: Any) -> bool: + """Check the subject is an annotation only attribute.""" + annotations = get_type_hints(parent, None, self.config.autodoc_type_aliases) + if self.objpath[-1] in annotations: + return True + else: + return False + + def import_object(self, raiseerror: bool = False) -> bool: + """Check the exisitence of uninitialized instance attribute when failed to import + the attribute.""" + try: + return super().import_object(raiseerror=True) # type: ignore + except ImportError as exc: + try: + ret = import_object(self.modname, self.objpath[:-1], 'class', + attrgetter=self.get_attr, # type: ignore + warningiserror=self.config.autodoc_warningiserror) + parent = ret[3] + if self.is_uninitialized_instance_attribute(parent): + self.object = UNINITIALIZED_ATTR + self.parent = parent + return True + except ImportError: + pass + + if raiseerror: + raise + else: + logger.warning(exc.args[0], type='autodoc', subtype='import_object') + self.env.note_reread() + return False + + def should_suppress_value_header(self) -> bool: + return (self.object is UNINITIALIZED_ATTR or + super().should_suppress_value_header()) + + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: + if self.object is UNINITIALIZED_ATTR: + return None + else: + return super().get_doc(encoding, ignore) # type: ignore + -class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # type: ignore +class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type: ignore + TypeVarMixin, RuntimeInstanceAttributeMixin, + UninitializedInstanceAttributeMixin, NonDataDescriptorMixin, + DocstringStripSignatureMixin, ClassLevelDocumenter): """ Specialized Documenter subclass for attributes. """ @@ -1741,6 +2382,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): member_order = 60 option_spec = dict(ModuleLevelDocumenter.option_spec) option_spec["annotation"] = annotation_option + option_spec["no-value"] = bool_option # must be higher than the MethodDocumenter, else it will recognize # some non-data descriptors as methods @@ -1765,56 +2407,144 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): def document_members(self, all_members: bool = False) -> None: pass - def import_object(self) -> Any: - ret = super().import_object() + def isinstanceattribute(self) -> bool: + """Check the subject is an instance attribute.""" + warnings.warn('AttributeDocumenter.isinstanceattribute() is deprecated.', + RemovedInSphinx50Warning) + # uninitialized instance variable (PEP-526) + with mock(self.config.autodoc_mock_imports): + try: + ret = import_object(self.modname, self.objpath[:-1], 'class', + attrgetter=self.get_attr, + warningiserror=self.config.autodoc_warningiserror) + self.parent = ret[3] + annotations = get_type_hints(self.parent, None, + self.config.autodoc_type_aliases) + if self.objpath[-1] in annotations: + self.object = UNINITIALIZED_ATTR + return True + except ImportError: + pass + + return False + + def update_annotations(self, parent: Any) -> None: + """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() + for (classname, attrname), annotation in analyzer.annotations.items(): + if classname == qualname and attrname not in annotations: + annotations[attrname] = annotation + except (AttributeError, PycodeError): + pass + except AttributeError: + pass + except TypeError: + # Failed to set __annotations__ (built-in, extensions, etc.) + pass + + def import_object(self, raiseerror: bool = False) -> bool: + ret = super().import_object(raiseerror) if inspect.isenumattribute(self.object): self.object = self.object.value - if inspect.isattributedescriptor(self.object): - self._datadescriptor = True - else: - # if it's not a data descriptor - self._datadescriptor = False + if self.parent: + self.update_annotations(self.parent) + return ret def get_real_modname(self) -> str: return self.get_attr(self.parent or self.object, '__module__', None) \ or self.modname + def should_suppress_value_header(self) -> bool: + if super().should_suppress_value_header(): + return True + else: + doc = self.get_doc() + if doc: + metadata = extract_metadata('\n'.join(sum(doc, []))) + if 'hide-value' in metadata: + return True + + return False + def add_directive_header(self, sig: str) -> None: super().add_directive_header(sig) sourcename = self.get_sourcename() - if not self.options.annotation: + if self.options.annotation is SUPPRESS or self.should_suppress_directive_header(): + pass + elif self.options.annotation: + self.add_line(' :annotation: %s' % self.options.annotation, sourcename) + else: # obtain type annotation for this attribute - annotations = getattr(self.parent, '__annotations__', {}) - if annotations and self.objpath[-1] in annotations: + annotations = get_type_hints(self.parent, None, self.config.autodoc_type_aliases) + if self.objpath[-1] in annotations: objrepr = stringify_typehint(annotations.get(self.objpath[-1])) self.add_line(' :type: ' + objrepr, sourcename) - else: - key = ('.'.join(self.objpath[:-1]), self.objpath[-1]) - if self.analyzer and key in self.analyzer.annotations: - self.add_line(' :type: ' + self.analyzer.annotations[key], - sourcename) - # data descriptors do not have useful values - if not self._datadescriptor: + try: + if self.options.no_value or self.should_suppress_value_header(): + pass + else: + objrepr = object_description(self.object) + self.add_line(' :value: ' + objrepr, sourcename) + except ValueError: + pass + + def get_attribute_comment(self, parent: Any, attrname: str) -> Optional[List[str]]: + try: + for cls in inspect.getmro(parent): try: - if self.object is INSTANCEATTR: - pass - else: - objrepr = object_description(self.object) - self.add_line(' :value: ' + objrepr, sourcename) - except ValueError: + module = safe_getattr(cls, '__module__') + qualname = safe_getattr(cls, '__qualname__') + + analyzer = ModuleAnalyzer.for_module(module) + analyzer.analyze() + if qualname and self.objpath: + key = (qualname, attrname) + if key in analyzer.attr_docs: + return list(analyzer.attr_docs[key]) + except (AttributeError, PycodeError): pass - elif self.options.annotation is SUPPRESS: + except (AttributeError, PycodeError): pass - else: - self.add_line(' :annotation: %s' % self.options.annotation, sourcename) - def add_content(self, more_content: Any, no_docstring: bool = False) -> None: - if not self._datadescriptor: - # if it's not a data descriptor, its docstring is very probably the - # wrong thing to display - no_docstring = True + return None + + def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]: + # Check the attribute has a docstring-comment + comment = self.get_attribute_comment(self.parent, self.objpath[-1]) + if comment: + return [comment] + + try: + # Disable `autodoc_inherit_docstring` temporarily to avoid to obtain + # a docstring from the value which descriptor returns unexpectedly. + # ref: https://github.com/sphinx-doc/sphinx/issues/7805 + orig = self.config.autodoc_inherit_docstrings + self.config.autodoc_inherit_docstrings = False # type: ignore + return super().get_doc(encoding, ignore) + finally: + self.config.autodoc_inherit_docstrings = orig # type: ignore + + def add_content(self, more_content: Optional[StringList], no_docstring: bool = False + ) -> None: + # Disable analyzing attribute comment on Documenter.add_content() to control it on + # AttributeDocumenter.add_content() + self.analyzer = None + + if more_content is None: + more_content = StringList() + self.update_content(more_content) super().add_content(more_content, no_docstring) @@ -1849,88 +2579,22 @@ class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # self.add_line(' :property:', sourcename) -class InstanceAttributeDocumenter(AttributeDocumenter): - """ - Specialized Documenter subclass for attributes that cannot be imported - because they are instance attributes (e.g. assigned in __init__). +class NewTypeAttributeDocumenter(AttributeDocumenter): """ - objtype = 'instanceattribute' - directivetype = 'attribute' - member_order = 60 - - # must be higher than AttributeDocumenter - priority = 11 - - @classmethod - def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any - ) -> bool: - """This documents only INSTANCEATTR members.""" - return (not isinstance(parent, ModuleDocumenter) and - isattr and - member is INSTANCEATTR) - - def import_object(self) -> bool: - """Never import anything.""" - # disguise as an attribute - self.objtype = 'attribute' - self.object = INSTANCEATTR - self._datadescriptor = False - return True - - def add_content(self, more_content: Any, no_docstring: bool = False) -> None: - """Never try to get a docstring from the object.""" - super().add_content(more_content, no_docstring=True) + Specialized Documenter subclass for NewTypes. - -class SlotsAttributeDocumenter(AttributeDocumenter): - """ - Specialized Documenter subclass for attributes that cannot be imported - because they are attributes in __slots__. + Note: This must be invoked before MethodDocumenter because NewType is a kind of + function object. """ - objtype = 'slotsattribute' - directivetype = 'attribute' - member_order = 60 - # must be higher than AttributeDocumenter - priority = 11 + objtype = 'newvarattribute' + directivetype = 'attribute' + priority = MethodDocumenter.priority + 1 @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any ) -> bool: - """This documents only SLOTSATTR members.""" - return member is SLOTSATTR - - def import_object(self) -> Any: - """Never import anything.""" - # disguise as an attribute - self.objtype = 'attribute' - self._datadescriptor = True - - with mock(self.env.config.autodoc_mock_imports): - try: - ret = import_object(self.modname, self.objpath[:-1], 'class', - attrgetter=self.get_attr, - warningiserror=self.env.config.autodoc_warningiserror) - self.module, _, _, self.parent = ret - return True - except ImportError as exc: - logger.warning(exc.args[0], type='autodoc', subtype='import_object') - self.env.note_reread() - return False - - def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]: - """Decode and return lines of the docstring(s) for the object.""" - if ignore is not None: - warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated." - % self.__class__.__name__, - RemovedInSphinx50Warning, stacklevel=2) - name = self.objpath[-1] - __slots__ = safe_getattr(self.parent, '__slots__', []) - if isinstance(__slots__, dict) and isinstance(__slots__.get(name), str): - docstring = prepare_docstring(__slots__[name]) - return [docstring] - else: - return [] + return not isinstance(parent, ModuleDocumenter) and inspect.isNewType(member) def get_documenters(app: Sphinx) -> Dict[str, "Type[Documenter]"]: @@ -1956,20 +2620,28 @@ def migrate_autodoc_member_order(app: Sphinx, config: Config) -> None: config.autodoc_member_order = 'alphabetical' # type: ignore +# for compatibility +from sphinx.ext.autodoc.deprecated import DataDeclarationDocumenter # NOQA +from sphinx.ext.autodoc.deprecated import GenericAliasDocumenter # NOQA +from sphinx.ext.autodoc.deprecated import InstanceAttributeDocumenter # NOQA +from sphinx.ext.autodoc.deprecated import SingledispatchFunctionDocumenter # NOQA +from sphinx.ext.autodoc.deprecated import SingledispatchMethodDocumenter # NOQA +from sphinx.ext.autodoc.deprecated import SlotsAttributeDocumenter # NOQA +from sphinx.ext.autodoc.deprecated import TypeVarDocumenter # NOQA + + def setup(app: Sphinx) -> Dict[str, Any]: app.add_autodocumenter(ModuleDocumenter) app.add_autodocumenter(ClassDocumenter) app.add_autodocumenter(ExceptionDocumenter) app.add_autodocumenter(DataDocumenter) - app.add_autodocumenter(DataDeclarationDocumenter) - app.add_autodocumenter(GenericAliasDocumenter) + app.add_autodocumenter(NewTypeDataDocumenter) app.add_autodocumenter(FunctionDocumenter) app.add_autodocumenter(DecoratorDocumenter) app.add_autodocumenter(MethodDocumenter) app.add_autodocumenter(AttributeDocumenter) app.add_autodocumenter(PropertyDocumenter) - app.add_autodocumenter(InstanceAttributeDocumenter) - app.add_autodocumenter(SlotsAttributeDocumenter) + app.add_autodocumenter(NewTypeAttributeDocumenter) app.add_config_value('autoclass_content', 'class', True, ENUM('both', 'class', 'init')) app.add_config_value('autodoc_member_order', 'alphabetical', True, @@ -1979,6 +2651,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('autodoc_mock_imports', [], True) app.add_config_value('autodoc_typehints', "signature", True, ENUM("signature", "description", "none")) + app.add_config_value('autodoc_type_aliases', {}, True) app.add_config_value('autodoc_warningiserror', True, True) app.add_config_value('autodoc_inherit_docstrings', True, True) app.add_event('autodoc-before-process-signature') |