1from sphinx.application import Sphinx
2from sphinx.environment import BuildEnvironment
3
4import os
5from typing import List, Set
6
7"""
8Store the modified time of the various doxygen xml files against the
9reStructuredText file that they are referenced from so that we know which
10reStructuredText files to rebuild if the doxygen xml is modified.
11
12We store the information in the environment object as 'breathe_file_state'
13so that it is pickled down and stored between builds as Sphinx is designed to do.
14
15(mypy doesn't like dynamically added attributes, hence all references to it are ignored)
16"""
17
18
19class MTimeError(Exception):
20    pass
21
22
23def _getmtime(filename: str):
24    try:
25        return os.path.getmtime(filename)
26    except OSError:
27        raise MTimeError('Cannot find file: %s' % os.path.realpath(filename))
28
29
30def update(app: Sphinx, source_file: str) -> None:
31    if not hasattr(app.env, "breathe_file_state"):
32        app.env.breathe_file_state = {}  # type: ignore
33
34    new_mtime = _getmtime(source_file)
35    mtime, docnames = app.env.breathe_file_state.setdefault(  # type: ignore
36        source_file, (new_mtime, set()))
37
38    assert app.env is not None
39    docnames.add(app.env.docname)
40
41    app.env.breathe_file_state[source_file] = (new_mtime, docnames)  # type: ignore
42
43
44def _get_outdated(app: Sphinx, env: BuildEnvironment,
45                  added: Set[str], changed: Set[str], removed: Set[str]) -> List[str]:
46    if not hasattr(app.env, "breathe_file_state"):
47        return []
48
49    stale = []
50    for filename, info in app.env.breathe_file_state.items():  # type: ignore
51        old_mtime, docnames = info
52        if _getmtime(filename) > old_mtime:
53            stale.extend(docnames)
54    return list(set(stale).difference(removed))
55
56
57def _purge_doc(app: Sphinx, env: BuildEnvironment, docname: str) -> None:
58    if not hasattr(app.env, "breathe_file_state"):
59        return
60
61    toremove = []
62    for filename, info in app.env.breathe_file_state.items():  # type: ignore
63        _, docnames = info
64        docnames.discard(docname)
65        if not docnames:
66            toremove.append(filename)
67
68    for filename in toremove:
69        del app.env.breathe_file_state[filename]  # type: ignore
70
71
72def setup(app: Sphinx):
73    app.connect("env-get-outdated", _get_outdated)
74    app.connect("env-purge-doc", _purge_doc)
75