@@ -335,6 +335,101 @@ def test_inplace_encoding_errors(self):
335335 with open (temp_file , 'rb' ) as f :
336336 self .assertEqual (f .read ().rstrip (b'\r \n ' ), b'New line ?' )
337337
338+ def check_inplace_name_taken (self , take_name ):
339+ # Something else gets the name of the file after it has been moved
340+ # to the backup file and before the output file is created.
341+ temp_file = self .writeTmp ('Initial text.' )
342+ self .addCleanup (os_helper .unlink , temp_file + '.bak' )
343+ orig_replace = os .replace
344+ def replace (src , dst ):
345+ orig_replace (src , dst )
346+ take_name (temp_file )
347+ with mock .patch ('os.replace' , replace ):
348+ with self .assertRaises (FileExistsError ):
349+ with FileInput (temp_file , inplace = True , encoding = "utf-8" ) as fi :
350+ fi .readline ()
351+ # The backup file is the only copy of the data, it must be kept.
352+ with open (temp_file + '.bak' , encoding = "utf-8" ) as f :
353+ self .assertEqual (f .read (), 'Initial text.' )
354+
355+ @os_helper .skip_unless_symlink
356+ def test_inplace_does_not_follow_symlink (self ):
357+ target = self .writeTmp ('Other text.' )
358+ os .chmod (target , 0o600 )
359+ mode = os .stat (target ).st_mode
360+ def take_name (name ):
361+ os .chmod (name + '.bak' , 0o644 )
362+ os .symlink (target , name )
363+ self .check_inplace_name_taken (take_name )
364+ with support .swap_attr (os , 'fstat' , UnconditionallyRaise (OSError )):
365+ self .check_inplace_name_taken (take_name )
366+ with open (target , encoding = "utf-8" ) as f :
367+ self .assertEqual (f .read (), 'Other text.' )
368+ self .assertEqual (os .stat (target ).st_mode , mode )
369+
370+ def test_inplace_does_not_reuse_existing_file (self ):
371+ names = []
372+ def take_name (name ):
373+ with open (name , 'w' , encoding = "utf-8" ) as f :
374+ f .write ('Other text.' )
375+ names .append (name )
376+ self .check_inplace_name_taken (take_name )
377+ with support .swap_attr (os , 'fstat' , UnconditionallyRaise (OSError )):
378+ self .check_inplace_name_taken (take_name )
379+ for name in names :
380+ with open (name , encoding = "utf-8" ) as f :
381+ self .assertEqual (f .read (), 'Other text.' )
382+
383+ def check_inplace_backup_kept (self , temp_file , exc_type ):
384+ with self .assertRaises (exc_type ):
385+ with FileInput (temp_file , inplace = True , encoding = "utf-8" ) as fi :
386+ fi .readline ()
387+ self .assertFalse (os .path .exists (temp_file ))
388+ with open (temp_file + '.bak' , encoding = "utf-8" ) as f :
389+ self .assertEqual (f .read (), 'Initial text.' )
390+
391+ def test_inplace_backup_kept_if_open_fails (self ):
392+ # The file can be moved to the backup file, but not opened.
393+ temp_file = self .writeTmp ('Initial text.' )
394+ self .addCleanup (os_helper .unlink , temp_file + '.bak' )
395+ with mock .patch ('fileinput.open' , create = True ,
396+ side_effect = PermissionError ):
397+ self .check_inplace_backup_kept (temp_file , PermissionError )
398+ # Trying again must not delete the backup file either.
399+ self .check_inplace_backup_kept (temp_file , FileNotFoundError )
400+ self .check_inplace_backup_kept (temp_file , FileNotFoundError )
401+
402+ def test_inplace_backup_kept_if_output_not_created (self ):
403+ temp_file = self .writeTmp ('Initial text.' )
404+ self .addCleanup (os_helper .unlink , temp_file + '.bak' )
405+ with mock .patch ('os.open' , side_effect = PermissionError ):
406+ self .check_inplace_backup_kept (temp_file , PermissionError )
407+ self .check_inplace_backup_kept (temp_file , FileNotFoundError )
408+ self .check_inplace_backup_kept (temp_file , FileNotFoundError )
409+
410+ @os_helper .skip_unless_symlink
411+ @unittest .skipUnless (os .chmod in os .supports_fd ,
412+ "os.chmod() does not support file descriptors" )
413+ @unittest .skipIf (sys .platform == 'win32' ,
414+ "cannot unlink an open file on Windows" )
415+ def test_inplace_chmod_does_not_follow_symlink (self ):
416+ # The output file is replaced by a symlink after it has been created
417+ # and before its permissions are set.
418+ target = self .writeTmp ('Other text.' )
419+ os .chmod (target , 0o600 )
420+ mode = os .stat (target ).st_mode
421+ temp_file = self .writeTmp ('Initial text.' )
422+ os .chmod (temp_file , 0o644 )
423+ orig_fdopen = os .fdopen
424+ def fdopen (fd , * args , ** kwargs ):
425+ os .unlink (temp_file )
426+ os .symlink (target , temp_file )
427+ return orig_fdopen (fd , * args , ** kwargs )
428+ with mock .patch ('os.fdopen' , fdopen ):
429+ with FileInput (temp_file , inplace = True , encoding = "utf-8" ) as fi :
430+ fi .readline ()
431+ self .assertEqual (os .stat (target ).st_mode , mode )
432+
338433 def test_file_hook_backward_compatibility (self ):
339434 def old_hook (filename , mode ):
340435 return io .StringIO ("I used to receive only filename and mode" )
0 commit comments