1""" 2 sphinx.directives 3 ~~~~~~~~~~~~~~~~~ 4 5 Handlers for additional ReST directives. 6 7 :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. 8 :license: BSD, see LICENSE for details. 9""" 10 11import re 12from typing import Any, Dict, Generic, List, Tuple, TypeVar, cast 13 14from docutils import nodes 15from docutils.nodes import Node 16from docutils.parsers.rst import directives, roles 17 18from sphinx import addnodes 19from sphinx.addnodes import desc_signature 20from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warning, 21 deprecated_alias) 22from sphinx.util import docutils 23from sphinx.util.docfields import DocFieldTransformer, Field, TypedField 24from sphinx.util.docutils import SphinxDirective 25from sphinx.util.typing import DirectiveOption 26 27if False: 28 # For type annotation 29 from sphinx.application import Sphinx 30 31 32# RE to strip backslash escapes 33nl_escape_re = re.compile(r'\\\n') 34strip_backslash_re = re.compile(r'\\(.)') 35 36T = TypeVar('T') 37 38 39def optional_int(argument: str) -> int: 40 """ 41 Check for an integer argument or None value; raise ``ValueError`` if not. 42 """ 43 if argument is None: 44 return None 45 else: 46 value = int(argument) 47 if value < 0: 48 raise ValueError('negative value; must be positive or zero') 49 return value 50 51 52class ObjectDescription(SphinxDirective, Generic[T]): 53 """ 54 Directive to describe a class, function or similar object. Not used 55 directly, but subclassed (in domain-specific directives) to add custom 56 behavior. 57 """ 58 59 has_content = True 60 required_arguments = 1 61 optional_arguments = 0 62 final_argument_whitespace = True 63 option_spec = { 64 'noindex': directives.flag, 65 } # type: Dict[str, DirectiveOption] 66 67 # types of doc fields that this directive handles, see sphinx.util.docfields 68 doc_field_types = [] # type: List[Field] 69 domain = None # type: str 70 objtype = None # type: str 71 indexnode = None # type: addnodes.index 72 73 # Warning: this might be removed in future version. Don't touch this from extensions. 74 _doc_field_type_map = {} # type: Dict[str, Tuple[Field, bool]] 75 76 def get_field_type_map(self) -> Dict[str, Tuple[Field, bool]]: 77 if self._doc_field_type_map == {}: 78 self._doc_field_type_map = {} 79 for field in self.doc_field_types: 80 for name in field.names: 81 self._doc_field_type_map[name] = (field, False) 82 83 if field.is_typed: 84 typed_field = cast(TypedField, field) 85 for name in typed_field.typenames: 86 self._doc_field_type_map[name] = (field, True) 87 88 return self._doc_field_type_map 89 90 def get_signatures(self) -> List[str]: 91 """ 92 Retrieve the signatures to document from the directive arguments. By 93 default, signatures are given as arguments, one per line. 94 """ 95 lines = nl_escape_re.sub('', self.arguments[0]).split('\n') 96 if self.config.strip_signature_backslash: 97 # remove backslashes to support (dummy) escapes; helps Vim highlighting 98 return [strip_backslash_re.sub(r'\1', line.strip()) for line in lines] 99 else: 100 return [line.strip() for line in lines] 101 102 def handle_signature(self, sig: str, signode: desc_signature) -> T: 103 """ 104 Parse the signature *sig* into individual nodes and append them to 105 *signode*. If ValueError is raised, parsing is aborted and the whole 106 *sig* is put into a single desc_name node. 107 108 The return value should be a value that identifies the object. It is 109 passed to :meth:`add_target_and_index()` unchanged, and otherwise only 110 used to skip duplicates. 111 """ 112 raise ValueError 113 114 def add_target_and_index(self, name: T, sig: str, signode: desc_signature) -> None: 115 """ 116 Add cross-reference IDs and entries to self.indexnode, if applicable. 117 118 *name* is whatever :meth:`handle_signature()` returned. 119 """ 120 return # do nothing by default 121 122 def before_content(self) -> None: 123 """ 124 Called before parsing content. Used to set information about the current 125 directive context on the build environment. 126 """ 127 pass 128 129 def transform_content(self, contentnode: addnodes.desc_content) -> None: 130 """ 131 Called after creating the content through nested parsing, 132 but before the ``object-description-transform`` event is emitted, 133 and before the info-fields are transformed. 134 Can be used to manipulate the content. 135 """ 136 pass 137 138 def after_content(self) -> None: 139 """ 140 Called after parsing content. Used to reset information about the 141 current directive context on the build environment. 142 """ 143 pass 144 145 def run(self) -> List[Node]: 146 """ 147 Main directive entry function, called by docutils upon encountering the 148 directive. 149 150 This directive is meant to be quite easily subclassable, so it delegates 151 to several additional methods. What it does: 152 153 * find out if called as a domain-specific directive, set self.domain 154 * create a `desc` node to fit all description inside 155 * parse standard options, currently `noindex` 156 * create an index node if needed as self.indexnode 157 * parse all given signatures (as returned by self.get_signatures()) 158 using self.handle_signature(), which should either return a name 159 or raise ValueError 160 * add index entries using self.add_target_and_index() 161 * parse the content and handle doc fields in it 162 """ 163 if ':' in self.name: 164 self.domain, self.objtype = self.name.split(':', 1) 165 else: 166 self.domain, self.objtype = '', self.name 167 self.indexnode = addnodes.index(entries=[]) 168 169 node = addnodes.desc() 170 node.document = self.state.document 171 node['domain'] = self.domain 172 # 'desctype' is a backwards compatible attribute 173 node['objtype'] = node['desctype'] = self.objtype 174 node['noindex'] = noindex = ('noindex' in self.options) 175 if self.domain: 176 node['classes'].append(self.domain) 177 178 self.names = [] # type: List[T] 179 signatures = self.get_signatures() 180 for i, sig in enumerate(signatures): 181 # add a signature node for each signature in the current unit 182 # and add a reference target for it 183 signode = addnodes.desc_signature(sig, '') 184 self.set_source_info(signode) 185 node.append(signode) 186 try: 187 # name can also be a tuple, e.g. (classname, objname); 188 # this is strictly domain-specific (i.e. no assumptions may 189 # be made in this base class) 190 name = self.handle_signature(sig, signode) 191 except ValueError: 192 # signature parsing failed 193 signode.clear() 194 signode += addnodes.desc_name(sig, sig) 195 continue # we don't want an index entry here 196 if name not in self.names: 197 self.names.append(name) 198 if not noindex: 199 # only add target and index entry if this is the first 200 # description of the object with this name in this desc block 201 self.add_target_and_index(name, sig, signode) 202 203 contentnode = addnodes.desc_content() 204 node.append(contentnode) 205 if self.names: 206 # needed for association of version{added,changed} directives 207 self.env.temp_data['object'] = self.names[0] 208 self.before_content() 209 self.state.nested_parse(self.content, self.content_offset, contentnode) 210 self.transform_content(contentnode) 211 self.env.app.emit('object-description-transform', 212 self.domain, self.objtype, contentnode) 213 DocFieldTransformer(self).transform_all(contentnode) 214 self.env.temp_data['object'] = None 215 self.after_content() 216 return [self.indexnode, node] 217 218 219class DefaultRole(SphinxDirective): 220 """ 221 Set the default interpreted text role. Overridden from docutils. 222 """ 223 224 optional_arguments = 1 225 final_argument_whitespace = False 226 227 def run(self) -> List[Node]: 228 if not self.arguments: 229 docutils.unregister_role('') 230 return [] 231 role_name = self.arguments[0] 232 role, messages = roles.role(role_name, self.state_machine.language, 233 self.lineno, self.state.reporter) 234 if role: 235 docutils.register_role('', role) 236 self.env.temp_data['default_role'] = role_name 237 else: 238 literal_block = nodes.literal_block(self.block_text, self.block_text) 239 reporter = self.state.reporter 240 error = reporter.error('Unknown interpreted text role "%s".' % role_name, 241 literal_block, line=self.lineno) 242 messages += [error] 243 244 return cast(List[nodes.Node], messages) 245 246 247class DefaultDomain(SphinxDirective): 248 """ 249 Directive to (re-)set the default domain for this source file. 250 """ 251 252 has_content = False 253 required_arguments = 1 254 optional_arguments = 0 255 final_argument_whitespace = False 256 option_spec = {} # type: Dict 257 258 def run(self) -> List[Node]: 259 domain_name = self.arguments[0].lower() 260 # if domain_name not in env.domains: 261 # # try searching by label 262 # for domain in env.domains.values(): 263 # if domain.label.lower() == domain_name: 264 # domain_name = domain.name 265 # break 266 self.env.temp_data['default_domain'] = self.env.domains.get(domain_name) 267 return [] 268 269from sphinx.directives.code import CodeBlock, Highlight, LiteralInclude # noqa 270from sphinx.directives.other import (Acks, Author, Centered, Class, HList, Include, # noqa 271 Only, SeeAlso, TabularColumns, TocTree, VersionChange) 272from sphinx.directives.patches import Figure, Meta # noqa 273from sphinx.domains.index import IndexDirective # noqa 274 275deprecated_alias('sphinx.directives', 276 { 277 'Highlight': Highlight, 278 'CodeBlock': CodeBlock, 279 'LiteralInclude': LiteralInclude, 280 'TocTree': TocTree, 281 'Author': Author, 282 'Index': IndexDirective, 283 'VersionChange': VersionChange, 284 'SeeAlso': SeeAlso, 285 'TabularColumns': TabularColumns, 286 'Centered': Centered, 287 'Acks': Acks, 288 'HList': HList, 289 'Only': Only, 290 'Include': Include, 291 'Class': Class, 292 'Figure': Figure, 293 'Meta': Meta, 294 }, 295 RemovedInSphinx40Warning, 296 { 297 'Highlight': 'sphinx.directives.code.Highlight', 298 'CodeBlock': 'sphinx.directives.code.CodeBlock', 299 'LiteralInclude': 'sphinx.directives.code.LiteralInclude', 300 'TocTree': 'sphinx.directives.other.TocTree', 301 'Author': 'sphinx.directives.other.Author', 302 'Index': 'sphinx.directives.other.IndexDirective', 303 'VersionChange': 'sphinx.directives.other.VersionChange', 304 'SeeAlso': 'sphinx.directives.other.SeeAlso', 305 'TabularColumns': 'sphinx.directives.other.TabularColumns', 306 'Centered': 'sphinx.directives.other.Centered', 307 'Acks': 'sphinx.directives.other.Acks', 308 'HList': 'sphinx.directives.other.HList', 309 'Only': 'sphinx.directives.other.Only', 310 'Include': 'sphinx.directives.other.Include', 311 'Class': 'sphinx.directives.other.Class', 312 'Figure': 'sphinx.directives.patches.Figure', 313 'Meta': 'sphinx.directives.patches.Meta', 314 }) 315 316deprecated_alias('sphinx.directives', 317 { 318 'DescDirective': ObjectDescription, 319 }, 320 RemovedInSphinx50Warning, 321 { 322 'DescDirective': 'sphinx.directives.ObjectDescription', 323 }) 324 325 326def setup(app: "Sphinx") -> Dict[str, Any]: 327 app.add_config_value("strip_signature_backslash", False, 'env') 328 directives.register_directive('default-role', DefaultRole) 329 directives.register_directive('default-domain', DefaultDomain) 330 directives.register_directive('describe', ObjectDescription) 331 # new, more consistent, name 332 directives.register_directive('object', ObjectDescription) 333 334 app.add_event('object-description-transform') 335 336 return { 337 'version': 'builtin', 338 'parallel_read_safe': True, 339 'parallel_write_safe': True, 340 } 341