# Based on http://stackoverflow.com/questions/2333872/atomic-writing-to-file-with-python

import os
from contextlib import contextmanager

# 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

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"):

    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.
        try:
            os.remove(dst)
        except OSError:
            pass
        os.rename(src, dst)


else:
    # POSIX rename() is always atomic
    from os import rename as replace


@contextmanager
def atomic_write(filepath, binary=False, fsync=False):
    """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.

    :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
    """

    tmppath = filepath + "~"
    while os.path.isfile(tmppath):
        tmppath += "~"
    try:
        with open(tmppath, "wb" if binary else "w") as file:
            yield file
            if fsync:
                file.flush()
                os.fsync(file.fileno())
        replace(tmppath, filepath)
    finally:
        try:
            os.remove(tmppath)
        except (IOError, OSError):
            pass
