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