def run(self): """Add a MarkdownNode when this directive is loaded """ env = self.state.document.settings.env node = MarkdownNode() options = Options(self.options) node.filename = options.get('filename') node.extensions = env.config.markdown_extensions node.load_markdown() return [node]
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 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