Sphinx

# Source code for sphinx.directives

    """Handlers for additional ReST directives."""

    from __future__ import annotations

    import re
    from typing import TYPE_CHECKING, cast

    from docutils import nodes
    from docutils.parsers.rst import directives, roles

    from sphinx import addnodes
    from sphinx.util import docutils
    from sphinx.util.docfields import DocFieldTransformer
    from sphinx.util.docutils import SphinxDirective

    if TYPE_CHECKING:
        from typing import ClassVar

        from docutils.nodes import Node

        from sphinx.addnodes import desc_signature
        from sphinx.application import Sphinx
        from sphinx.util.docfields import Field, TypedField
        from sphinx.util.typing import ExtensionMetadata, OptionSpec

    # RE to strip backslash escapes
    nl_escape_re = re.compile(r'\\\n')
    strip_backslash_re = re.compile(r'\\(.)')

    def optional_int(argument: str) -> int | None:
        """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:
                msg = 'negative value; must be positive or zero'
                raise ValueError(msg)
            return value

    class ObjectDescription[ObjDescT](SphinxDirective):
        """Directive to describe a class, function or similar object.

        Not used directly, but subclassed (in domain-specific directives)
        to add custom behaviour.
        """

        has_content = True
        required_arguments = 1
        optional_arguments = 0
        final_argument_whitespace = True
        option_spec: ClassVar[OptionSpec] = {
            'no-index': directives.flag,
            'no-index-entry': directives.flag,
            'no-contents-entry': directives.flag,
            'no-typesetting': directives.flag,
            'noindex': directives.flag,
            'noindexentry': directives.flag,
            'nocontentsentry': directives.flag,
        }

        # types of doc fields that this directive handles, see sphinx.util.docfields
        doc_field_types: list[Field] = []
        domain: str | None = None
        objtype: str  # set when `run` method is called
        indexnode: addnodes.index

        # Warning: this might be removed in future version. Don't touch this from extensions.
        _doc_field_type_map: 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) -> ObjDescT:
            """Parse the signature *sig*.

            The individual nodes are then appended 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: ObjDescT, sig: str, signode: desc_signature
        ) -> None:
            """Add cross-reference IDs and entries to self.indexnode, if applicable.

            *name* is whatever :meth:`handle_signature()` returned.
            """
            pass  # 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, content_node: addnodes.desc_content) -> None:
            """Can be used to manipulate the content.

            Called after creating the content through nested parsing,
            but before the ``object-description-transform`` event is emitted,
            and before the info-fields are transformed.
            """
            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 _object_hierarchy_parts(self, sig_node: desc_signature) -> tuple[str, ...]:
            """Returns a tuple of strings, one entry for each part of the object's
            hierarchy (e.g. ``('module', 'submodule', 'Class', 'method')``). The
            returned tuple is used to properly nest children within parents in the
            table of contents, and can also be used within the
            :py:meth:`_toc_entry_name` method.

            This method must not be used outwith table of contents generation.
            """
            return ()

        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 `no-index`
            * 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.partition(':')
            else:
                self.domain, self.objtype = '', self.name
            self.indexnode = addnodes.index(entries=[])

            node = addnodes.desc()
            node.document = self.state.document
            source, line = self.get_source_info()
            # If any options were specified to the directive,
            # self.state.document.current_line will at this point be set to
            # None.  To ensure nodes created as part of the signature have a line
            # number set, set the document's line number correctly.
            #
            # Note that we need to subtract one from the line number since
            # note_source uses 0-based line numbers.
            if line is not None:
                line -= 1
            self.state.document.note_source(source, line)  # type: ignore[arg-type]
            node['domain'] = self.domain
            # 'desctype' is a backwards compatible attribute
            node['objtype'] = node['desctype'] = self.objtype

            # Copy old option names to new ones
            # xref RemovedInSphinx90Warning
            # deprecate noindex, noindexentry, and nocontentsentry in Sphinx 9.0
            if 'no-index' not in self.options and 'noindex' in self.options:
                self.options['no-index'] = self.options['noindex']
            if 'no-index-entry' not in self.options and 'noindexentry' in self.options:
                self.options['no-index-entry'] = self.options['noindexentry']
            if (
                'no-contents-entry' not in self.options
                and 'nocontentsentry' in self.options
            ):
                self.options['no-contents-entry'] = self.options['nocontentsentry']

            node['no-index'] = node['noindex'] = no_index = 'no-index' in self.options
            node['no-index-entry'] = node['noindexentry'] = 'no-index-entry' in self.options
            node['no-contents-entry'] = node['nocontentsentry'] = (
                'no-contents-entry' in self.options
            )
            node['no-typesetting'] = 'no-typesetting' in self.options
            if self.domain:
                node['classes'].append(self.domain)
            node['classes'].append(node['objtype'])

            self.names: list[ObjDescT] = []
            signatures = self.get_signatures()
            for sig in 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
                finally:
                    # Private attributes for ToC generation. Will be modified or removed
                    # without notice.
                    if self.config.toc_object_entries:
                        signode['_toc_parts'] = self._object_hierarchy_parts(signode)
                        signode['_toc_name'] = self._toc_entry_name(signode)
                    else:
                        signode['_toc_parts'] = ()
                        signode['_toc_name'] = ''
                if name not in self.names:
                    self.names.append(name)
                    if not no_index:
                        # 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)

            if self.names:
                # needed for association of version{added,changed} directives
                object_name: ObjDescT = self.names[0]
                if isinstance(object_name, tuple):
                    self.env.current_document.obj_desc_name = str(object_name[0])
                else:
                    self.env.current_document.obj_desc_name = str(object_name)
            self.before_content()
            content_children = self.parse_content_to_nodes(allow_section_headings=True)
            content_node = addnodes.desc_content('', *content_children)
            node.append(content_node)
            self.transform_content(content_node)
            self.env.events.emit(
                'object-description-transform', self.domain, self.objtype, content_node
            )
            DocFieldTransformer(self).transform_all(content_node)
            self.env.current_document.obj_desc_name = ''
            self.after_content()

            if node['no-typesetting']:
                # Attempt to return the index node, and a new target node
                # containing all the ids of this node and its children.
                # If ``:no-index:`` is set, or there are no ids on the node
                # or any of its children, then just return the index node,
                # as Docutils expects a target node to have at least one id.
                if node_ids := [  # type: ignore[var-annotated]
                    node_id
                    for el in node.findall(nodes.Element)
                    for node_id in el.get('ids', ())
                ]:
                    target_node = nodes.target(ids=node_ids)
                    self.set_source_info(target_node)
                    return [self.indexnode, target_node]
                return [self.indexnode]
            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]
            # TODO: TYPING: Upstream docutils should widen roles.role() to accept
            #       the RST language module shape (has a `roles` mapping);
            #       current stub requires _LanguageModule from docutils.languages.
            role, messages = roles.role(
                role_name,
                self.state_machine.language,  # type: ignore[arg-type]
                self.lineno,
                self.state.reporter,
            )
            if role:
                docutils.register_role('', role)  # type: ignore[arg-type]
                self.env.current_document.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: ClassVar[OptionSpec] = {}

        def run(self) -> list[Node]:
            domain_name = self.arguments[0].lower()
            default_domain = self.env.domains.get(domain_name)
            self.env.current_document.default_domain = default_domain
            return []

    def setup(app: Sphinx) -> ExtensionMetadata:
        app.add_config_value(
            'strip_signature_backslash', False, 'env', types=frozenset({bool})
        )
        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,
        }