class FixtureDocumenter(FunctionDocumenter): """ Sphinx autodoc :class:`~sphinx.ext.autodoc.Documenter` for documenting pytest fixtures. """ objtype = "fixture" directivetype = "fixture" priority = 20 object: Union[FunctionType, MethodType] # noqa: A003 # pylint: disable=redefined-builtin def __init__(self, directive: DocumenterBridge, name: str, indent: str = '') -> None: super().__init__(directive, name, indent) self.options = Options(self.options.copy()) @classmethod def can_document_member( cls, member: Any, membername: str, isattr: bool, parent: Any, ) -> bool: """ Called to see if a member can be documented by this documenter. :param member: The member being checked. :param membername: The name of the member. :param isattr: :param parent: The parent of the member. """ if isinstance(member, FunctionType): return is_fixture(member)[0] else: # pragma: no cover return False def add_directive_header(self, sig: str = '') -> None: """ Add the directive's header. :param sig: Unused -- fixtures have no useful signature. :no-default sig: """ # doc_lines = (self.object.__doc__ or '').splitlines() # docstring = StringList([dedent(doc_lines[0]) + dedent("\n".join(doc_lines))[1:]]) # print(docstring) # input(">>>") super().add_directive_header('') self.add_line('', self.get_sourcename()) self.add_line( f" **Scope:** |nbsp| |nbsp| |nbsp| |nbsp| {is_fixture(self.object)[1]}", self.get_sourcename(), )
class TypedDictDocumenter(ClassDocumenter): r""" Sphinx autodoc :class:`~sphinx.ext.autodoc.Documenter` for documenting :class:`typing.TypedDict`\s. """ # noqa: D400 objtype = "typeddict" directivetype = "typeddict" priority = 20 option_spec: Dict[str, Callable] = { "noindex": flag, "alphabetical": flag, "show-inheritance": flag, } def __init__(self, *args: Any) -> None: super().__init__(*args) self.options = Options(self.options.copy()) alphabetical = self.options.get("alphabetical", False) if alphabetical: self.options["member-order"] = "alphabetical" else: self.options["member-order"] = "bysource" for key in {"inherited-members", "special-members"}: # pragma: no cover if key in self.options: del self.options[key] @classmethod def can_document_member( cls, member: Any, membername: str, isattr: bool, parent: Any, ) -> bool: """ Called to see if a member can be documented by this documenter. :param member: The member being checked. :param membername: The name of the member. :param isattr: :param parent: The parent of the member. """ for attr in {"__optional_keys__", "__required_keys__", "__total__"}: if not hasattr(member, attr): return False return True def format_signature(self, **kwargs: Any) -> str: """ Typed Dicts do not have a signature. """ return '' def add_content(self, more_content: Any, no_docstring: bool = False): """ Add the autodocumenter content. :param more_content: :param no_docstring: """ if self.doc_as_attr: # pragma: no cover (verbatim from Sphinx) 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) more_content = docutils.statemachine.StringList( [_("alias of :class:`%s`") % classname], source='') no_docstring = True # set sourcename and add content from attribute documentation sourcename = self.get_sourcename() if self.analyzer: attr_docs = self.analyzer.find_attr_docs() if self.objpath: key = ('.'.join(self.objpath[:-1]), self.objpath[-1]) if key in attr_docs: no_docstring = True # make a copy of docstring for attributes to avoid cache # the change of autodoc-process-docstring event. docstrings = [list(attr_docs[key])] for i, line in enumerate(self.process_doc(docstrings)): self.add_line(line, sourcename, i) # add content from docstrings if not no_docstring: docstrings = self.get_doc() or [] 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([":class:`typing.TypedDict`.", '']) 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: # pragma: no cover (verbatim from Sphinx) for line, src in zip(more_content.data, more_content.items): self.add_line(line, src[0], src[1]) def document_members(self, all_members: bool = False) -> None: """ Generate reST for member documentation. All members are always documented. """ super().document_members(True) def sort_members( self, documenters: List[Tuple[Documenter, bool]], order: str, ) -> List[Tuple[Documenter, bool]]: """ Sort the TypedDict's members. :param documenters: :param order: """ # The documenters for the keys, in the desired order documenters = super().sort_members(documenters, order) # Mapping of key names to docstrings (as list of strings) docstrings = { k[1]: v for k, v in ModuleAnalyzer.for_module( self.object.__module__).find_attr_docs().items() } required_keys = [] optional_keys = [] types = get_type_hints(self.object) for d in documenters: name = d[0].name.split('.')[-1] if name in self.object.__required_keys__: required_keys.append(name) elif name in self.object.__optional_keys__: optional_keys.append(name) # else: warn user. This shouldn't ever happen, though. sourcename = self.get_sourcename() if required_keys: self.add_line('', sourcename) self.add_line(":Required Keys:", sourcename) self.document_keys(required_keys, types, docstrings) self.add_line('', sourcename) if optional_keys: self.add_line('', sourcename) self.add_line(":Optional Keys:", sourcename) self.document_keys(optional_keys, types, docstrings) self.add_line('', sourcename) return [] def document_keys(self, keys: List[str], types: Dict[str, Type], docstrings: Dict[str, List[str]]): """ Document keys in a :class:`typing.TypedDict`. :param keys: List of key names to document. :param types: Mapping of key names to types. :param docstrings: Mapping of key names to docstrings. """ content = StringList() for key in keys: if key in types: key_type = f"({format_annotation(types[key])}) " else: key_type = '' if key in docstrings: content.append( f" * **{key}** {key_type}-- {' '.join(docstrings.get(key, ''))}" ) else: content.append(f" * **{key}** {key_type}") for line in content: self.add_line(line, self.get_sourcename()) def filter_members( self, members: ObjectMembers, want_all: bool, ) -> List[Tuple[str, Any, bool]]: """ Filter the given member list. :param members: :param want_all: """ ret = [] # process members and determine which to skip for (membername, member) in members: # if isattr is True, the member is documented as an attribute if safe_getattr(member, "__sphinx_mock__", False): # mocked module or object keep = False elif membername.startswith('_'): keep = False else: keep = True # give the user a chance to decide whether this member # should be skipped if self.env.app: # let extensions preprocess docstrings try: skip_user = self.env.app.emit_firstresult( "autodoc-skip-member", self.objtype, membername, member, not keep, self.options, ) if skip_user is not None: keep = not skip_user except Exception as exc: filter_members_warning(member, exc) keep = False if keep: ret.append((membername, member, member is INSTANCEATTR)) return ret
class TypedAttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): """ Alternative version of :class:`sphinx.ext.autodoc.AttributeDocumenter` with better type hint rendering. Specialized Documenter subclass for attributes. .. versionadded:: 0.7.0 .. versionchanged:: 1.0.0 Now uses the type of the variable if it is not explicitly annotated. """ # noqa: D400 objtype = "attribute" member_order = 60 option_spec = dict(ModuleLevelDocumenter.option_spec) option_spec["annotation"] = annotation_option # must be higher than the MethodDocumenter, else it will recognize # some non-data descriptors as methods priority = 10 def __init__(self, directive: DocumenterBridge, name: str, indent: str = '') -> None: super().__init__(directive=directive, name=name, indent=indent) self.options = Options(self.options.copy()) self._datadescriptor = True @staticmethod def is_function_or_method(obj: Any) -> bool: # noqa: D102 return inspect.isfunction(obj) or inspect.isbuiltin( obj) or inspect.ismethod(obj) @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any) -> bool: """ Called to see if a member can be documented by this documenter. """ if inspect.isattributedescriptor(member): return True elif (not isinstance(parent, ModuleDocumenter) and not inspect.isroutine(member) and not isinstance(member, type)): return True else: return False def document_members(self, all_members: bool = False) -> None: # noqa: D102 pass def isinstanceattribute(self) -> bool: """ Check the subject is an instance attribute. """ try: analyzer = ModuleAnalyzer.for_module(self.modname) attr_docs = analyzer.find_attr_docs() if self.objpath: key = ('.'.join(self.objpath[:-1]), self.objpath[-1]) if key in attr_docs: return True return False except PycodeError: return False 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: :py:obj:`True` if successful, :py:obj:`False` if an error occurred. """ try: ret = super().import_object(raiseerror=True) 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 except ImportError as exc: if self.isinstanceattribute(): self.object = INSTANCEATTR self._datadescriptor = False ret = True elif raiseerror: raise else: logger.warning(exc.args[0], type="autodoc", subtype="import_object") self.env.note_reread() ret = False return ret def get_real_modname(self) -> str: """ Get the real module name of an object to document. It can differ from the name of the module through which the object was imported. """ return self.get_attr(self.parent or self.object, "__module__", None) or self.modname def add_directive_header(self, sig: str): """ Add the directive's header. :param sig: """ sourcename = self.get_sourcename() no_value = self.options.get("no-value", False) no_type = self.options.get("no-type", False) if not self.options.get("annotation", ''): ClassLevelDocumenter.add_directive_header(self, sig) # data descriptors do not have useful values if not no_value and not self._datadescriptor: if "value" in self.options: self.add_line(" :value: " + self.options["value"], sourcename) else: with suppress(ValueError): if self.object is not INSTANCEATTR: objrepr = object_description(self.object) self.add_line(" :value: " + objrepr, sourcename) self.add_line('', sourcename) if not no_type: if "type" in self.options: self.add_line(type_template % self.options["type"], sourcename) else: # obtain type annotation for this attribute the_type = get_variable_type(self) if not the_type.strip(): obj_type = type(self.object) if obj_type is object: return try: the_type = format_annotation(obj_type) except Exception: return line = type_template % the_type self.add_line(line, sourcename) else: super().add_directive_header(sig) def get_doc( # type: ignore self, encoding: Optional[str] = None, ignore: Optional[int] = None, ) -> List[List[str]]: """ Decode and return lines of the docstring(s) for the object. :param encoding: :param ignore: """ 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.env.config.autodoc_inherit_docstrings self.env.config.autodoc_inherit_docstrings = False # type: ignore # Sphinx's signature is wrong wrt Optional if sphinx.version_info >= (4, 0): if encoding is not None: raise TypeError( "The 'encoding' argument to get_doc was removed in Sphinx 4" ) else: return super().get_doc(ignore=cast(int, ignore)) or [] else: return super().get_doc(cast(str, encoding), cast( int, ignore)) or [] # type: ignore finally: self.env.config.autodoc_inherit_docstrings = orig # type: ignore def add_content(self, more_content: Any, no_docstring: bool = False) -> None: """ Add content from docstrings, attribute documentation and user. """ with warnings.catch_warnings(): # TODO: work out what to do about this warnings.simplefilter("ignore", RemovedInSphinx50Warning) if not self._datadescriptor: # if it's not a data descriptor, its docstring is very probably the # wrong thing to display no_docstring = True super().add_content(more_content, no_docstring)
class VariableDocumenter(DataDocumenter): """ Specialized Documenter subclass for data items. """ directivetype = "data" objtype = "variable" priority: float = DataDocumenter.priority + 0.5 # type: ignore # keeps it below TypeVarDocumenter option_spec = { "no-value": flag, "no-type": flag, "type": str, "value": str, **DataDocumenter.option_spec, } def __init__(self, directive: DocumenterBridge, name: str, indent: str = '') -> None: super().__init__(directive=directive, name=name, indent=indent) self.options = Options(self.options.copy()) add_nbsp_substitution(self.env.app.config) def add_directive_header(self, sig: str): """ Add the directive's header. :param sig: """ sourcename = self.get_sourcename() no_value = self.options.get("no-value", False) no_type = self.options.get("no-type", False) if not self.options.get("annotation", ''): ModuleLevelDocumenter.add_directive_header(self, sig) if not no_value: if "value" in self.options: self.add_line(f" :value: {self.options['value']}", sourcename) else: with suppress(ValueError): if self.object is not UNINITIALIZED_ATTR: objrepr = object_description(self.object) self.add_line(f" :value: {objrepr}", sourcename) self.add_line('', sourcename) if not no_type: if "type" in self.options: the_type = self.options["type"] else: # obtain type annotation for this data the_type = get_variable_type(self) if not the_type.strip(): obj_type = type(self.object) if obj_type is object: return try: the_type = format_annotation(obj_type) except Exception: return line = type_template % the_type self.add_line(line, sourcename) else: super().add_directive_header(sig)
class TypedAttributeDocumenter(AttributeDocumenter): """ Alternative version of :class:`sphinx.ext.autodoc.AttributeDocumenter` with better type hint rendering. Specialized Documenter subclass for attributes. .. versionadded:: 0.7.0 .. versionchanged:: 1.0.0 Now uses the type of the variable if it is not explicitly annotated. """ # noqa D400 def __init__(self, directive: DocumenterBridge, name: str, indent: str = '') -> None: super().__init__(directive=directive, name=name, indent=indent) self.options = Options(self.options.copy()) self._datadescriptor = True def add_directive_header(self, sig: str): """ Add the directive's header. :param sig: """ sourcename = self.get_sourcename() no_value = self.options.get("no-value", False) no_type = self.options.get("no-type", False) if not self.options.get("annotation", ''): ClassLevelDocumenter.add_directive_header(self, sig) # data descriptors do not have useful values if not no_value and not self._datadescriptor: if "value" in self.options: self.add_line(" :value: " + self.options["value"], sourcename) else: try: if self.object is not INSTANCEATTR: objrepr = object_description(self.object) self.add_line(" :value: " + objrepr, sourcename) except ValueError: pass self.add_line('', sourcename) if not no_type: if "type" in self.options: self.add_line(type_template % self.options["type"], sourcename) else: # obtain type annotation for this attribute the_type = get_variable_type(self) if not the_type.strip(): try: the_type = format_annotation(type(self.object)) except Exception: return line = type_template % the_type self.add_line(line, sourcename) else: super().add_directive_header(sig)
class NamedTupleDocumenter(ClassDocumenter): r""" Sphinx autodoc :class:`~sphinx.ext.autodoc.Documenter` for documenting :class:`typing.NamedTuple`\s. .. versionadded:: 0.8.0 .. versionchanged:: 0.1.0 Will no longer attempt to find attribute docstrings from other namedtuple classes. """ # noqa: D400 objtype = "namedtuple" directivetype = "namedtuple" priority = 20 object: Type # noqa: A003 # pylint: disable=redefined-builtin def __init__(self, directive: DocumenterBridge, name: str, indent: str = '') -> None: super().__init__(directive, name, indent) self.options = Options(self.options.copy()) @classmethod def can_document_member( cls, member: Any, membername: str, isattr: bool, parent: Any, ) -> bool: """ Called to see if a member can be documented by this documenter. :param member: The member being checked. :param membername: The name of the member. :param isattr: :param parent: The parent of the member. """ return is_namedtuple(member) def add_content(self, more_content: Any, no_docstring: bool = True): r""" Add extra content (from docstrings, attribute docs etc.), but not the :class:`typing.NamedTuple`\'s docstring. :param more_content: :param no_docstring: """ # noqa: D400 with warnings.catch_warnings(): # TODO: work out what to do about this warnings.simplefilter("ignore", RemovedInSphinx50Warning) Documenter.add_content(self, more_content, True) # set sourcename and add content from attribute documentation sourcename = self.get_sourcename() params, pre_output, post_output = self._get_docstring() for line in pre_output: self.add_line(line, sourcename) def _get_docstring(self) -> Tuple[Dict[str, Param], List[str], List[str]]: """ Returns params, pre_output, post_output. """ # Size varies depending on docutils config tab_size = self.env.app.config.docutils_tab_width if self.object.__doc__: docstring = dedent( self.object.__doc__).expandtabs(tab_size).split('\n') elif "show-inheritance" not in self.options: docstring = [":class:`typing.NamedTuple`."] else: docstring = [''] docstring = list(self.process_doc([docstring])) return parse_parameters(docstring, tab_size=tab_size) def add_directive_header(self, sig: str) -> None: """ Add the directive's header, and the inheritance information if the ``:show-inheritance:`` flag set. :param sig: The NamedTuple's signature. """ super().add_directive_header(sig) if "show-inheritance" not in self.options: return acceptable_bases = { " Bases: :class:`tuple`", " Bases: :py:class:`tuple`", " Bases: :class:`tuple`, :class:`typing.Generic`", " Bases: :py:class:`tuple`, :py:class:`typing.Generic`", " Bases: :class:`NamedTuple`", " Bases: :py:class:`NamedTuple`", } # TODO: support generic bases for multiple inheritance if self.directive.result[ -1] in acceptable_bases or baseclass_is_private(self.object): # TODO: multiple inheritance if getattr(self.env.config, "generic_bases_fully_qualified", False): # Might not be present; extension might not be enabled prefix = '' else: prefix = '~' if hasattr(self.object, "__annotations__"): self.directive.result[ -1] = f" Bases: :class:`{prefix}typing.NamedTuple`" else: self.directive.result[ -1] = f" Bases: :func:`{prefix}collections.namedtuple`" def filter_members( self, members: ObjectMembers, want_all: bool, ) -> List[Tuple[str, Any, bool]]: """ Filter the list of members to always include ``__new__`` if it has a different signature to the tuple. :param members: :param want_all: """ all_hints = get_type_hints(self.object) class_hints = { k: all_hints[k] for k in self.object._fields if k in all_hints } # TODO: need a better way to partially resolve type hints, and warn about failures new_hints = get_type_hints( self.object.__new__, globalns=sys.modules[self.object.__module__].__dict__, localns=self.object.__dict__, ) # Stock NamedTuples don't have these, but customised collections.namedtuple or hand-rolled classes may if "cls" in new_hints: new_hints.pop("cls") if "return" in new_hints: new_hints.pop("return") if class_hints and new_hints and class_hints != new_hints: #: __new__ has a different signature or different annotations def unskip_new(app, what, name, obj, skip, options): if name == "__new__": return False return None listener_id = self.env.app.connect("autodoc-skip-member", unskip_new) members_ = super().filter_members(members, want_all) self.env.app.disconnect(listener_id) return members_ else: return super().filter_members(members, want_all) def sort_members( self, documenters: List[Tuple[Documenter, bool]], order: str, ) -> List[Tuple[Documenter, bool]]: r""" Sort the :class:`typing.NamedTuple`\'s members. :param documenters: :param order: """ # The documenters for the fields and methods, in the desired order # The fields will be in bysource order regardless of the order option documenters = super().sort_members(documenters, order) # Size varies depending on docutils config a_tab = ' ' * self.env.app.config.docutils_tab_width # Mapping of member names to docstrings (as list of strings) member_docstrings: Dict[str, List[str]] try: namedtuple_source = textwrap.dedent(inspect.getsource(self.object)) # Mapping of member names to docstrings (as list of strings) member_docstrings = { k[1]: v for k, v in ModuleAnalyzer.for_string( namedtuple_source, self.object.__module__).find_attr_docs().items() } except (TypeError, OSError): member_docstrings = {} # set sourcename and add content from attribute documentation sourcename = self.get_sourcename() params, pre_output, post_output = self._get_docstring() self.add_line('', sourcename) self.add_line(":Fields:", sourcename) # TODO: Add xref targets for each field as an attribute # TODO: support for default_values self.add_line('', sourcename) fields = self.object._fields for pos, field in enumerate(fields): doc: List[str] = [''] arg_type: str = '' # Prefer doc from class docstring if field in params: doc, arg_type = params.pop(field).values() # type: ignore # Otherwise use attribute docstring if not ''.join(doc).strip() and field in member_docstrings: doc = member_docstrings[field] # Fallback to namedtuple's default docstring if not ''.join(doc).strip(): doc = [getattr(self.object, field).__doc__] # Prefer annotations over docstring types type_hints = get_type_hints(self.object) if type_hints: if field in type_hints: arg_type = format_annotation(type_hints[field]) field_entry = [f"{a_tab}{pos})", "|nbsp|", f"**{field}**"] if arg_type: field_entry.append(f"({arg_type}\\)") field_entry.append("--") field_entry.extend(doc) if field_alias_re.match(getattr(self.object, field).__doc__ or ''): getattr(self.object, field).__doc__ = ' '.join(doc) self.add_line(' '.join(field_entry), sourcename) self.add_line('', sourcename) for line in post_output: self.add_line(line, sourcename) self.add_line('', sourcename) # Remove documenters corresponding to fields and return the rest return [ d for d in documenters if d[0].name.split('.')[-1] not in fields ]
class ProtocolDocumenter(ClassDocumenter): r""" Sphinx autodoc :class:`~sphinx.ext.autodoc.Documenter` for documenting :class:`typing.Protocol`\s. .. versionadded:: 0.2.0 """ # noqa D400 objtype = "protocol" directivetype = "protocol" priority = 20 option_spec: Dict[str, Callable] = { "noindex": flag, "member-order": member_order_option, "show-inheritance": flag, "exclude-protocol-members": exclude_members_option, } def __init__(self, directive: DocumenterBridge, name: str, indent: str = '') -> None: super().__init__(directive, name, indent) self.options = Options(self.options.copy()) @classmethod def can_document_member( cls, member: Any, membername: str, isattr: bool, parent: Any, ) -> bool: """ Called to see if a member can be documented by this documenter. :param member: The member being checked. :param membername: The name of the member. :param isattr: :param parent: The parent of the member. """ # _is_protocol = True return isinstance(member, _ProtocolMeta) def format_signature(self, **kwargs: Any) -> str: """ Protocols do not have a signature. """ return '' # pragma: no cover def add_content(self, more_content: Any, no_docstring: bool = False): """ Add the autodocumenter content. :param more_content: :param no_docstring: """ super().add_content(more_content=more_content, no_docstring=no_docstring) sourcename = self.get_sourcename() if not getdoc(self.object) and "show-inheritance" not in self.options: self.add_line(":class:`typing.Protocol`.", sourcename) self.add_line('', sourcename) if hasattr( self.object, "_is_runtime_protocol") and self.object._is_runtime_protocol: self.add_line(runtime_message, sourcename) self.add_line('', sourcename) self.add_line( "Classes that implement this protocol must have the following methods / attributes:", sourcename) self.add_line('', sourcename) def document_members(self, all_members: bool = False) -> None: """ Generate reST for member documentation. All members are always documented. """ super().document_members(True) def filter_members( self, members: List[Tuple[str, Any]], want_all: bool, ) -> List[Tuple[str, Any, bool]]: """ Filter the given member list. :param members: :param want_all: """ ret = [] # process members and determine which to skip for (membername, member) in members: # if isattr is True, the member is documented as an attribute if safe_getattr(member, "__sphinx_mock__", False): # mocked module or object keep = False elif (self.options.get("exclude-protocol-members", []) and membername in self.options["exclude-protocol-members"]): # remove members given by exclude-protocol-members keep = False elif membername.startswith('_') and not ( membername.startswith("__") and membername.endswith("__")): keep = False elif membername not in globally_excluded_methods: # Magic method you wouldn't overload, or private method. if membername in dir(self.object.__base__): keep = member is not getattr(self.object.__base__, membername) else: keep = True else: keep = False # give the user a chance to decide whether this member # should be skipped if self.env.app: # let extensions preprocess docstrings try: skip_user = self.env.app.emit_firstresult( "autodoc-skip-member", self.objtype, membername, member, not keep, self.options, ) if skip_user is not None: keep = not skip_user except Exception as exc: filter_members_warning(member, exc) keep = False if keep: ret.append((membername, member, member is INSTANCEATTR)) return ret