From d91ba115bdd47c265e082c35a7d39d5455687431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 23 Mar 2024 14:57:32 +0100 Subject: [PATCH] Verify that an asset file to be copied exists (#12183) --- CHANGES.rst | 3 +++ sphinx/util/osutil.py | 13 +++++++++++-- tests/test_builders/test_build_html.py | 24 ++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4549b9f68..69845d33e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -131,6 +131,9 @@ Bugs fixed may be used, when multiple suffixes are specified in :confval:`source_suffix`. Patch by Sutou Kouhei. +* #10786: improve the error message when a file to be copied (e.g., an asset) + is removed during Sphinx execution. + Patch by Bénédikt Tran. Testing ------- diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index 93fc62528..97a298ed0 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -11,13 +11,14 @@ import sys import unicodedata from io import StringIO from os import path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from sphinx.deprecation import _deprecation_warning if TYPE_CHECKING: from collections.abc import Iterator from types import TracebackType + from typing import Any # SEP separates path elements in the canonical file names # @@ -89,8 +90,16 @@ def copytimes(source: str | os.PathLike[str], dest: str | os.PathLike[str]) -> N def copyfile(source: str | os.PathLike[str], dest: str | os.PathLike[str]) -> None: """Copy a file and its modification times, if possible. - Note: ``copyfile`` skips copying if the file has not been changed + :param source: An existing source to copy. + :param dest: The destination path. + :raise FileNotFoundError: The *source* does not exist. + + .. note:: :func:`copyfile` is a no-op if *source* and *dest* are identical. """ + if not path.exists(source): + msg = f'{os.fsdecode(source)} does not exist' + raise FileNotFoundError(msg) + if not path.exists(dest) or not filecmp.cmp(source, dest): shutil.copyfile(source, dest) with contextlib.suppress(OSError): diff --git a/tests/test_builders/test_build_html.py b/tests/test_builders/test_build_html.py index 0d88645d9..9a3b0c8bf 100644 --- a/tests/test_builders/test_build_html.py +++ b/tests/test_builders/test_build_html.py @@ -1,5 +1,6 @@ """Test the HTML builder and check output against XPath.""" +import os import posixpath import re @@ -8,6 +9,7 @@ import pytest from sphinx.builders.html import validate_html_extra_path, validate_html_static_path from sphinx.deprecation import RemovedInSphinx80Warning from sphinx.errors import ConfigError +from sphinx.util.console import strip_colors from sphinx.util.inventory import InventoryFile FIGURE_CAPTION = ".//figure/figcaption/p" @@ -387,3 +389,25 @@ def test_html_signaturereturn_icon(app): content = (app.outdir / 'index.html').read_text(encoding='utf8') assert ('' in content) + + +@pytest.mark.sphinx('html', testroot='root', srcdir=os.urandom(4).hex()) +def test_html_remove_sources_before_write_gh_issue_10786(app, warning): + # see: https://github.com/sphinx-doc/sphinx/issues/10786 + target = app.srcdir / 'img.png' + + def handler(app): + assert target.exists() + target.unlink() + return [] + + app.connect('html-collect-pages', handler) + assert target.exists() + app.build() + assert not target.exists() + + ws = strip_colors(warning.getvalue()).splitlines() + assert len(ws) >= 1 + + file = os.fsdecode(target) + assert f'WARNING: cannot copy image file {file!r}: {file!s} does not exist' == ws[-1]