[Python-Dev] "with" use case: replacing a file

Ka-Ping Yee python-dev at zesty.ca
Thu May 12 22:00:24 CEST 2005


Here's another use case to think about.

Example 2: Replacing a File.

Suppose we want to reliably replace a file.  We require that either:

    (a) The file is completely replaced with the new contents;
 or (b) the filesystem is unchanged and a meaningful exception is thrown.

We'd like to be able to write this conveniently as:

    with replace(filename) as file:
        ...
        file.write(spam)
        ...
        file.write(eggs)
        ...

To make sure the file is never only partially written, we rely on the
filesystem to rename files atomically, so the basic steps (without
error handling) look like this:

    tempname = filename + '.tmp'
    file = open(tempname, 'w')
    ...
    file.write(spam)
    ...
    file.close()
    os.rename(tempname, filename)

We would like to make sure the temporary file is cleaned up and no
filehandles are left open.  Here's my analysis of all the execution
paths we need to cover:

    1. +open +write +close +rename
    2. +open +write +close -rename ?remove
    3. +open +write -close ?remove
    4. +open -write +close ?remove
    5. +open -write -close ?remove
    6. -open

(In this list, + means success, - means failure, ? means don't care.)

When i add error handling, i get this:

    tempname = filename + '.tmp'
    file = open(tempname, 'w') # okay to let exceptions escape
    problem = None
    try:
        try:
            ...
            file.write(spam)
            ...
        except:
            problem = sys.exc_info()
            raise problem
    finally:
        try:
            file.close()
        except Exception, exc:
            problem, problem.reason = exc, problem
        if not problem:
            try:
                os.rename(tempname, filename)
            except Exception, exc:
                problem, problem.reason = exc, problem
        if problem:
            try:
                os.remove(tempname)
            except Exception, exc:
                problem, problem.reason = exc, problem
                raise problem

In this case, the implementation of replace() doesn't require a
separate __except__ method:

class replace:
    def __init__(self, filename):
        self.filename = filename
        self.tempname = '%s.%d.%d' % (self.filename, os.getpid(), id(self))

    def __enter__(self):
        self.file = open(self.tempname, 'w')
        return self

    def write(self, data):
        self.file.write(data)

    def __exit__(self, *problem):
        try:
            self.file.close()
        except Exception, exc:
            problem, problem.reason = exc, problem
        if not problem: # commit
            try:
                os.rename(tempname, filename)
            except Exception, exc:
                problem, problem.reason = exc, problem
        if problem: # rollback
            try:
                os.remove(tempname)
            except Exception, exc:
                problem, problem.reason = exc, problem
                raise problem

This is all so intricate i'm not sure if i got it right.  Somebody
let me know if this looks right or not.  (In any case, i look forward
to the day when i can rely on someone else to get it right, and they
only have to write it once!)


-- ?!ng


More information about the Python-Dev mailing list