diff --git a/CHANGES b/CHANGES index c2c0bd45c..948b3dd6e 100644 --- a/CHANGES +++ b/CHANGES @@ -137,6 +137,7 @@ Features added * html: Output ``canonical_url`` metadata if :confval:`html_baseurl` set (refs: #4193) * #5029: autosummary: expose ``inherited_members`` to template +* #4362: latex: Don't overwrite .tex file if document not changed Bugs fixed ---------- diff --git a/sphinx/builders/latex/__init__.py b/sphinx/builders/latex/__init__.py index 32e813241..382914731 100644 --- a/sphinx/builders/latex/__init__.py +++ b/sphinx/builders/latex/__init__.py @@ -13,7 +13,6 @@ import os from os import path from docutils.frontend import OptionParser -from docutils.io import FileOutput from six import text_type from sphinx import package_dir, addnodes, highlighting @@ -31,7 +30,7 @@ from sphinx.locale import _, __ from sphinx.transforms import SphinxTransformer from sphinx.util import texescape, logging, status_iterator from sphinx.util.console import bold, darkgreen # type: ignore -from sphinx.util.docutils import new_document +from sphinx.util.docutils import SphinxFileOutput, new_document from sphinx.util.fileutil import copy_asset_file from sphinx.util.nodes import inline_all_toctrees from sphinx.util.osutil import SEP, make_filename @@ -134,9 +133,8 @@ class LaTeXBuilder(Builder): toctree_only = False if len(entry) > 5: toctree_only = entry[5] - destination = FileOutput( - destination_path=path.join(self.outdir, targetname), - encoding='utf-8') + destination = SphinxFileOutput(destination_path=path.join(self.outdir, targetname), + encoding='utf-8', overwrite_if_changed=True) logger.info(__("processing %s..."), targetname, nonl=1) toctrees = self.env.get_doctree(docname).traverse(addnodes.toctree) if toctrees: diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index d96ab843c..547d74c17 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -10,6 +10,7 @@ """ from __future__ import absolute_import +import codecs import os import re import types @@ -21,6 +22,7 @@ from os import path import docutils from docutils import nodes +from docutils.io import FileOutput from docutils.parsers.rst import Directive, directives, roles, convert_directive_function from docutils.statemachine import StateMachine from docutils.utils import Reporter @@ -300,6 +302,26 @@ def switch_source_input(state, content): state.memo.reporter.get_source_and_line = get_source_and_line +class SphinxFileOutput(FileOutput): + """Better FileOutput class for Sphinx.""" + + def __init__(self, **kwargs): + # type: (Any) -> None + self.overwrite_if_changed = kwargs.pop('overwrite_if_changed', False) + FileOutput.__init__(self, **kwargs) + + def write(self, data): + # type: (unicode) -> unicode + if (self.destination_path and self.autoclose and 'b' not in self.mode and + self.overwrite_if_changed and os.path.exists(self.destination_path)): + with codecs.open(self.destination_path, encoding=self.encoding) as f: + # skip writing: content not changed + if f.read() == data: + return data + + return FileOutput.write(self, data) + + class SphinxDirective(Directive): """A base class for Sphinx directives. diff --git a/tests/test_util_docutils.py b/tests/test_util_docutils.py index 31a1d9bd2..9319863e0 100644 --- a/tests/test_util_docutils.py +++ b/tests/test_util_docutils.py @@ -9,9 +9,11 @@ :license: BSD, see LICENSE for details. """ +import os + from docutils import nodes -from sphinx.util.docutils import docutils_namespace, register_node +from sphinx.util.docutils import SphinxFileOutput, docutils_namespace, register_node def test_register_node(): @@ -32,3 +34,31 @@ def test_register_node(): assert not hasattr(nodes.GenericNodeVisitor, 'depart_custom_node') assert not hasattr(nodes.SparseNodeVisitor, 'visit_custom_node') assert not hasattr(nodes.SparseNodeVisitor, 'depart_custom_node') + + +def test_SphinxFileOutput(tmpdir): + content = 'Hello Sphinx World' + + # write test.txt at first + filename = str(tmpdir / 'test.txt') + output = SphinxFileOutput(destination_path=filename) + output.write(content) + os.utime(filename, (0, 0)) + + # overrite it again + output.write(content) + assert os.stat(filename).st_mtime != 0 # updated + + # write test2.txt at first + filename = str(tmpdir / 'test2.txt') + output = SphinxFileOutput(destination_path=filename, overwrite_if_changed=True) + output.write(content) + os.utime(filename, (0, 0)) + + # overrite it again + output.write(content) + assert os.stat(filename).st_mtime == 0 # not updated + + # overrite it again (content changed) + output.write(content + "; content change") + assert os.stat(filename).st_mtime != 0 # updated