2016-05-26 20:49:58 -07:00
# Based on http://stackoverflow.com/questions/2333872/atomic-writing-to-file-with-python
2016-05-06 18:19:16 -07:00
import os
from contextlib import contextmanager
2016-05-26 20:49:58 -07:00
# We would ideally atomically replace any existing file with the new
# version. However, on Windows there's no Python-only solution prior
# to Python 3.3. (This library includes a C extension to do so:
# https://pypi.python.org/pypi/pyosreplace/0.1.)
#
# Correspondingly, we make a best effort, but on Python < 3.3 use a
# replace method which could result in the file temporarily
# disappearing.
import sys
2021-07-29 02:26:34 +02:00
2016-05-26 20:49:58 -07:00
if sys . version_info > = ( 3 , 3 ) :
# Python 3.3 and up have a native `replace` method
from os import replace
elif sys . platform . startswith ( " win " ) :
2021-07-29 02:26:34 +02:00
2016-05-26 20:49:58 -07:00
def replace ( src , dst ) :
# TODO: on Windows, this will raise if the file is in use,
# which is possible. We'll need to make this more robust over
# time.
2016-05-26 21:49:29 -07:00
try :
os . remove ( dst )
except OSError :
pass
2016-05-26 20:49:58 -07:00
os . rename ( src , dst )
2021-07-29 02:26:34 +02:00
2016-05-26 20:49:58 -07:00
else :
# POSIX rename() is always atomic
from os import rename as replace
2021-07-29 02:26:34 +02:00
2016-05-06 18:19:16 -07:00
@contextmanager
def atomic_write ( filepath , binary = False , fsync = False ) :
2021-07-29 02:26:34 +02:00
""" Writeable file object that atomically updates a file (using a temporary file). In some cases (namely Python < 3.3 on Windows), this could result in an existing file being temporarily unlinked.
2016-05-06 18:19:16 -07:00
: param filepath : the file path to be opened
: param binary : whether to open the file in a binary mode instead of textual
: param fsync : whether to force write the file to disk
"""
2021-07-29 02:26:34 +02:00
tmppath = filepath + " ~ "
2016-05-06 18:19:16 -07:00
while os . path . isfile ( tmppath ) :
2021-07-29 02:26:34 +02:00
tmppath + = " ~ "
2016-05-06 18:19:16 -07:00
try :
2021-07-29 02:26:34 +02:00
with open ( tmppath , " wb " if binary else " w " ) as file :
2016-05-06 18:19:16 -07:00
yield file
if fsync :
file . flush ( )
os . fsync ( file . fileno ( ) )
2016-05-26 20:49:58 -07:00
replace ( tmppath , filepath )
2016-05-06 18:19:16 -07:00
finally :
try :
os . remove ( tmppath )
except ( IOError , OSError ) :
pass