@@ -39,6 +39,12 @@ def copy2_safe(src, dst, log=None):
3939
4040 like shutil.copy2, but log errors in copystat instead of raising
4141 """
42+ # if src file is not writable, avoid creating a back-up
43+ if not os .access (src , os .W_OK ):
44+ if log :
45+ log .debug ("Source file, %s, is not writable" , src , exc_info = True )
46+ raise PermissionError (errno .EACCES , f"File is not writable: { src } " )
47+
4248 shutil .copyfile (src , dst )
4349 try :
4450 shutil .copystat (src , dst )
@@ -52,6 +58,11 @@ async def async_copy2_safe(src, dst, log=None):
5258
5359 like shutil.copy2, but log errors in copystat instead of raising
5460 """
61+ if not os .access (src , os .W_OK ):
62+ if log :
63+ log .debug ("Source file, %s, is not writable" , src , exc_info = True )
64+ raise PermissionError (errno .EACCES , f"File is not writable: { src } " )
65+
5566 await run_sync (shutil .copyfile , src , dst )
5667 try :
5768 await run_sync (shutil .copystat , src , dst )
@@ -101,6 +112,21 @@ def atomic_writing(path, text=True, encoding="utf-8", log=None, **kwargs):
101112 if os .path .islink (path ):
102113 path = os .path .join (os .path .dirname (path ), os .readlink (path ))
103114
115+ # Fall back to direct write for existing file in a non-writable dir
116+ dirpath = os .path .dirname (path ) or os .getcwd ()
117+ if os .path .isfile (path ) and not os .access (dirpath , os .W_OK ) and os .access (path , os .W_OK ):
118+ mode = "w" if text else "wb"
119+ # direct open on the target file
120+ if text :
121+ fileobj = open (path , mode , encoding = encoding , ** kwargs ) # noqa: SIM115
122+ else :
123+ fileobj = open (path , mode , ** kwargs ) # noqa: SIM115
124+ try :
125+ yield fileobj
126+ finally :
127+ fileobj .close ()
128+ return
129+
104130 tmp_path = path_to_intermediate (path )
105131
106132 if os .path .isfile (path ):
0 commit comments