""" sphinx.directives ~~~~~~~~~~~~~~~~~ Handlers for additional ReST directives. :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re from typing import TYPE_CHECKING, Any, Dict, Generic, List, Tuple, TypeVar, cast from docutils import nodes from docutils.nodes import Node from docutils.parsers.rst import directives, roles from sphinx import addnodes from sphinx.addnodes import desc_signature from sphinx.deprecation import RemovedInSphinx50Warning, deprecated_alias from sphinx.util import docutils from sphinx.util.docfields import DocFieldTransformer, Field, TypedField from sphinx.util.docutils import SphinxDirective from sphinx.util.typing import OptionSpec if TYPE_CHECKING: from sphinx.application import Sphinx # RE to strip backslash escapes nl_escape_re = re.compile(r'\\\n') strip_backslash_re = re.compile(r'\\(.)') T = TypeVar('T') def optional_int(argument: str) -> int: """ Check for an integer argument or None value; raise ``ValueError`` if not. """ if argument is None: return None else: value = int(argument) if value < 0: raise ValueError('negative value; must be positive or zero') return value class ObjectDescription(SphinxDirective, Generic[T]): """ Directive to describe a class, function or similar object. Not used directly, but subclassed (in domain-specific directives) to add custom behavior. """ has_content = True required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True option_spec: OptionSpec = { 'noindex': directives.flag, } # types of doc fields that this directive handles, see sphinx.util.docfields doc_field_types = [] # type: List[Field] domain = None # type: str objtype = None # type: str indexnode = None # type: addnodes.index # Warning: this might be removed in future version. Don't touch this from extensions. _doc_field_type_map = {} # type: Dict[str, Tuple[Field, bool]] def get_field_type_map(self) -> Dict[str, Tuple[Field, bool]]: if self._doc_field_type_map == {}: self._doc_field_type_map = {} for field in self.doc_field_types: for name in field.names: self._doc_field_type_map[name] = (field, False) if field.is_typed: typed_field = cast(TypedField, field) for name in typed_field.typenames: self._doc_field_type_map[name] = (field, True) return self._doc_field_type_map def get_signatures(self) -> List[str]: """ Retrieve the signatures to document from the directive arguments. By default, signatures are given as arguments, one per line. """ lines = nl_escape_re.sub('', self.arguments[0]).split('\n') if self.config.strip_signature_backslash: # remove backslashes to support (dummy) escapes; helps Vim highlighting return [strip_backslash_re.sub(r'\1', line.strip()) for line in lines] else: return [line.strip() for line in lines] def handle_signature(self, sig: str, signode: desc_signature) -> T: """ Parse the signature *sig* into individual nodes and append them to *signode*. If ValueError is raised, parsing is aborted and the whole *sig* is put into a single desc_name node. The return value should be a value that identifies the object. It is passed to :meth:`add_target_and_index()` unchanged, and otherwise only used to skip duplicates. """ raise ValueError def add_target_and_index(self, name: T, sig: str, signode: desc_signature) -> None: """ Add cross-reference IDs and entries to self.indexnode, if applicable. *name* is whatever :meth:`handle_signature()` returned. """ return # do nothing by default def before_content(self) -> None: """ Called before parsing content. Used to set information about the current directive context on the build environment. """ pass def transform_content(self, contentnode: addnodes.desc_content) -> None: """ Called after creating the content through nested parsing, but before the ``object-description-transform`` event is emitted, and before the info-fields are transformed. Can be used to manipulate the content. """ pass def after_content(self) -> None: """ Called after parsing content. Used to reset information about the current directive context on the build environment. """ pass def run(self) -> List[Node]: """ Main directive entry function, called by docutils upon encountering the directive. This directive is meant to be quite easily subclassable, so it delegates to several additional methods. What it does: * find out if called as a domain-specific directive, set self.domain * create a `desc` node to fit all description inside * parse standard options, currently `noindex` * create an index node if needed as self.indexnode * parse all given signatures (as returned by self.get_signatures()) using self.handle_signature(), which should either return a name or raise ValueError * add index entries using self.add_target_and_index() * parse the content and handle doc fields in it """ if ':' in self.name: self.domain, self.objtype = self.name.split(':', 1) else: self.domain, self.objtype = '', self.name self.indexnode = addnodes.index(entries=[]) node = addnodes.desc() node.document = self.state.document node['domain'] = self.domain # 'desctype' is a backwards compatible attribute node['objtype'] = node['desctype'] = self.objtype node['noindex'] = noindex = ('noindex' in self.options) if self.domain: node['classes'].append(self.domain) self.names = [] # type: List[T] signatures = self.get_signatures() for i, sig in enumerate(signatures): # add a signature node for each signature in the current unit # and add a reference target for it signode = addnodes.desc_signature(sig, '') self.set_source_info(signode) node.append(signode) try: # name can also be a tuple, e.g. (classname, objname); # this is strictly domain-specific (i.e. no assumptions may # be made in this base class) name = self.handle_signature(sig, signode) except ValueError: # signature parsing failed signode.clear() signode += addnodes.desc_name(sig, sig) continue # we don't want an index entry here if name not in self.names: self.names.append(name) if not noindex: # only add target and index entry if this is the first # description of the object with this name in this desc block self.add_target_and_index(name, sig, signode) contentnode = addnodes.desc_content() node.append(contentnode) if self.names: # needed for association of version{added,changed} directives self.env.temp_data['object'] = self.names[0] self.before_content() self.state.nested_parse(self.content, self.content_offset, contentnode) self.transform_content(contentnode) self.env.app.emit('object-description-transform', self.domain, self.objtype, contentnode) DocFieldTransformer(self).transform_all(contentnode) self.env.temp_data['object'] = None self.after_content() return [self.indexnode, node] class DefaultRole(SphinxDirective): """ Set the default interpreted text role. Overridden from docutils. """ optional_arguments = 1 final_argument_whitespace = False def run(self) -> List[Node]: if not self.arguments: docutils.unregister_role('') return [] role_name = self.arguments[0] role, messages = roles.role(role_name, self.state_machine.language, self.lineno, self.state.reporter) if role: docutils.register_role('', role) self.env.temp_data['default_role'] = role_name else: literal_block = nodes.literal_block(self.block_text, self.block_text) reporter = self.state.reporter error = reporter.error('Unknown interpreted text role "%s".' % role_name, literal_block, line=self.lineno) messages += [error] return cast(List[nodes.Node], messages) class DefaultDomain(SphinxDirective): """ Directive to (re-)set the default domain for this source file. """ has_content = False required_arguments = 1 optional_arguments = 0 final_argument_whitespace = False option_spec: OptionSpec = {} def run(self) -> List[Node]: domain_name = self.arguments[0].lower() # if domain_name not in env.domains: # # try searching by label # for domain in env.domains.values(): # if domain.label.lower() == domain_name: # domain_name = domain.name # break self.env.temp_data['default_domain'] = self.env.domains.get(domain_name) return [] deprecated_alias('sphinx.directives', { 'DescDirective': ObjectDescription, }, RemovedInSphinx50Warning, { 'DescDirective': 'sphinx.directives.ObjectDescription', }) def setup(app: "Sphinx") -> Dict[str, Any]: app.add_config_value("strip_signature_backslash", False, 'env') directives.register_directive('default-role', DefaultRole) directives.register_directive('default-domain', DefaultDomain) directives.register_directive('describe', ObjectDescription) # new, more consistent, name directives.register_directive('object', ObjectDescription) app.add_event('object-description-transform') return { 'version': 'builtin', 'parallel_read_safe': True, 'parallel_write_safe': True, }