1""" 2 sphinx.domains.changeset 3 ~~~~~~~~~~~~~~~~~~~~~~~~ 4 5 The changeset domain. 6 7 :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. 8 :license: BSD, see LICENSE for details. 9""" 10 11from collections import namedtuple 12from typing import Any, Dict, List, cast 13 14from docutils import nodes 15from docutils.nodes import Node 16 17from sphinx import addnodes 18from sphinx.domains import Domain 19from sphinx.locale import _ 20from sphinx.util.docutils import SphinxDirective 21 22if False: 23 # For type annotation 24 from sphinx.application import Sphinx 25 from sphinx.environment import BuildEnvironment 26 27 28versionlabels = { 29 'versionadded': _('New in version %s'), 30 'versionchanged': _('Changed in version %s'), 31 'deprecated': _('Deprecated since version %s'), 32} 33 34versionlabel_classes = { 35 'versionadded': 'added', 36 'versionchanged': 'changed', 37 'deprecated': 'deprecated', 38} 39 40 41# TODO: move to typing.NamedTuple after dropping py35 support (see #5958) 42ChangeSet = namedtuple('ChangeSet', 43 ['type', 'docname', 'lineno', 'module', 'descname', 'content']) 44 45 46class VersionChange(SphinxDirective): 47 """ 48 Directive to describe a change/addition/deprecation in a specific version. 49 """ 50 has_content = True 51 required_arguments = 1 52 optional_arguments = 1 53 final_argument_whitespace = True 54 option_spec = {} # type: Dict 55 56 def run(self) -> List[Node]: 57 node = addnodes.versionmodified() 58 node.document = self.state.document 59 self.set_source_info(node) 60 node['type'] = self.name 61 node['version'] = self.arguments[0] 62 text = versionlabels[self.name] % self.arguments[0] 63 if len(self.arguments) == 2: 64 inodes, messages = self.state.inline_text(self.arguments[1], 65 self.lineno + 1) 66 para = nodes.paragraph(self.arguments[1], '', *inodes, translatable=False) 67 self.set_source_info(para) 68 node.append(para) 69 else: 70 messages = [] 71 if self.content: 72 self.state.nested_parse(self.content, self.content_offset, node) 73 classes = ['versionmodified', versionlabel_classes[self.name]] 74 if len(node): 75 if isinstance(node[0], nodes.paragraph) and node[0].rawsource: 76 content = nodes.inline(node[0].rawsource, translatable=True) 77 content.source = node[0].source 78 content.line = node[0].line 79 content += node[0].children 80 node[0].replace_self(nodes.paragraph('', '', content, translatable=False)) 81 82 para = cast(nodes.paragraph, node[0]) 83 para.insert(0, nodes.inline('', '%s: ' % text, classes=classes)) 84 else: 85 para = nodes.paragraph('', '', 86 nodes.inline('', '%s.' % text, 87 classes=classes), 88 translatable=False) 89 node.append(para) 90 91 domain = cast(ChangeSetDomain, self.env.get_domain('changeset')) 92 domain.note_changeset(node) 93 94 ret = [node] # type: List[Node] 95 ret += messages 96 return ret 97 98 99class ChangeSetDomain(Domain): 100 """Domain for changesets.""" 101 102 name = 'changeset' 103 label = 'changeset' 104 105 initial_data = { 106 'changes': {}, # version -> list of ChangeSet 107 } # type: Dict 108 109 @property 110 def changesets(self) -> Dict[str, List[ChangeSet]]: 111 return self.data.setdefault('changes', {}) # version -> list of ChangeSet 112 113 def note_changeset(self, node: addnodes.versionmodified) -> None: 114 version = node['version'] 115 module = self.env.ref_context.get('py:module') 116 objname = self.env.temp_data.get('object') 117 changeset = ChangeSet(node['type'], self.env.docname, node.line, 118 module, objname, node.astext()) 119 self.changesets.setdefault(version, []).append(changeset) 120 121 def clear_doc(self, docname: str) -> None: 122 for version, changes in self.changesets.items(): 123 for changeset in changes[:]: 124 if changeset.docname == docname: 125 changes.remove(changeset) 126 127 def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None: 128 # XXX duplicates? 129 for version, otherchanges in otherdata['changes'].items(): 130 changes = self.changesets.setdefault(version, []) 131 for changeset in otherchanges: 132 if changeset.docname in docnames: 133 changes.append(changeset) 134 135 def process_doc(self, env: "BuildEnvironment", docname: str, document: nodes.document) -> None: # NOQA 136 pass # nothing to do here. All changesets are registered on calling directive. 137 138 def get_changesets_for(self, version: str) -> List[ChangeSet]: 139 return self.changesets.get(version, []) 140 141 142def setup(app: "Sphinx") -> Dict[str, Any]: 143 app.add_domain(ChangeSetDomain) 144 app.add_directive('deprecated', VersionChange) 145 app.add_directive('versionadded', VersionChange) 146 app.add_directive('versionchanged', VersionChange) 147 148 return { 149 'version': 'builtin', 150 'env_version': 1, 151 'parallel_read_safe': True, 152 'parallel_write_safe': True, 153 } 154