88
99from docutils .utils import relative_path
1010
11+ from sphinx .util import logging
1112from sphinx .util .osutil import copyfile , ensuredir
1213
1314if TYPE_CHECKING :
1415 from sphinx .util .template import BaseRenderer
1516 from sphinx .util .typing import PathMatcher
1617
18+ logger = logging .getLogger (__name__ )
19+
1720
1821def _template_basename (filename : str | os .PathLike [str ]) -> str | None :
1922 """Given an input filename:
@@ -30,7 +33,8 @@ def _template_basename(filename: str | os.PathLike[str]) -> str | None:
3033
3134def copy_asset_file (source : str | os .PathLike [str ], destination : str | os .PathLike [str ],
3235 context : dict [str , Any ] | None = None ,
33- renderer : BaseRenderer | None = None ) -> None :
36+ renderer : BaseRenderer | None = None ,
37+ * , __overwrite_warning__ : bool = True ) -> None :
3438 """Copy an asset file to destination.
3539
3640 On copying, it expands the template variables if context argument is given and
@@ -56,17 +60,36 @@ def copy_asset_file(source: str | os.PathLike[str], destination: str | os.PathLi
5660 renderer = SphinxRenderer ()
5761
5862 with open (source , encoding = 'utf-8' ) as fsrc :
59- destination = _template_basename (destination ) or destination
60- with open (destination , 'w' , encoding = 'utf-8' ) as fdst :
61- fdst .write (renderer .render_string (fsrc .read (), context ))
63+ template_content = fsrc .read ()
64+ rendered_template = renderer .render_string (template_content , context )
65+
66+ if (
67+ __overwrite_warning__
68+ and os .path .exists (destination )
69+ and template_content != rendered_template
70+ ):
71+ # Consider raising an error in Sphinx 8.
72+ # Certainly make overwriting user content opt-in.
73+ # xref: RemovedInSphinx80Warning
74+ # xref: https://github.com/sphinx-doc/sphinx/issues/12096
75+ msg = ('Copying the rendered template %s to %s will overwrite data, '
76+ 'as a file already exists at the destination path '
77+ 'and the content does not match.' )
78+ logger .info (msg , os .fsdecode (source ), os .fsdecode (destination ),
79+ type = 'misc' , subtype = 'copy_overwrite' )
80+
81+ destination = _template_basename (destination ) or destination
82+ with open (destination , 'w' , encoding = 'utf-8' ) as fdst :
83+ fdst .write (rendered_template )
6284 else :
63- copyfile (source , destination )
85+ copyfile (source , destination , __overwrite_warning__ = __overwrite_warning__ )
6486
6587
6688def copy_asset (source : str | os .PathLike [str ], destination : str | os .PathLike [str ],
6789 excluded : PathMatcher = lambda path : False ,
6890 context : dict [str , Any ] | None = None , renderer : BaseRenderer | None = None ,
69- onerror : Callable [[str , Exception ], None ] | None = None ) -> None :
91+ onerror : Callable [[str , Exception ], None ] | None = None ,
92+ * , __overwrite_warning__ : bool = True ) -> None :
7093 """Copy asset files to destination recursively.
7194
7295 On copying, it expands the template variables if context argument is given and
@@ -88,7 +111,8 @@ def copy_asset(source: str | os.PathLike[str], destination: str | os.PathLike[st
88111
89112 ensuredir (destination )
90113 if os .path .isfile (source ):
91- copy_asset_file (source , destination , context , renderer )
114+ copy_asset_file (source , destination , context , renderer ,
115+ __overwrite_warning__ = __overwrite_warning__ )
92116 return
93117
94118 for root , dirs , files in os .walk (source , followlinks = True ):
@@ -104,7 +128,8 @@ def copy_asset(source: str | os.PathLike[str], destination: str | os.PathLike[st
104128 try :
105129 copy_asset_file (posixpath .join (root , filename ),
106130 posixpath .join (destination , reldir ),
107- context , renderer )
131+ context , renderer ,
132+ __overwrite_warning__ = __overwrite_warning__ )
108133 except Exception as exc :
109134 if onerror :
110135 onerror (posixpath .join (root , filename ), exc )
0 commit comments