1

At the start of my script, the file script.log is expected to exist and messages are being appended to it. But if the file does not exist (the user has decided to delete the file), the file should not be created again, the messages will be printed to stdout instead. Fill the bucket only if you see it.

How to do that?
Loads of people must have had a similar problem but I couldn't find any solution.


I aimed at something like the following code but the append mode does not trigger any exception when the file is not found:
try:
    with open('script.log', 'a') as f:
        f.write('foo')
except FileNotFoundError:
    print('foo')

I know that the following code should work but ideally I would like to avoid it because it contains a race condition:

if os.path.exists('script.log'):
    with open('script.log', 'a') as f:
        f.write('foo')
else:
    print('foo')
Jeyekomon
  • 2,878
  • 2
  • 27
  • 37
  • 3
    What race condition do you have in mind? – Henry Harutyunyan Jan 14 '20 at 14:22
  • 1
    @HenryHarutyunyan [This one](https://stackoverflow.com/questions/14574518/how-does-using-the-try-statement-avoid-a-race-condition). – Jeyekomon Jan 14 '20 at 14:28
  • In unix file API you can open an existing file and/or create it. If you don't specify create and the file does not exist then the open will fail. I'm sure this behaviour is exposed in python? – Gem Taylor Jan 14 '20 at 14:29
  • @Jeyekomon interesting! Do you delete the file somewhere in your code? I'm thinking of how the condition may harm your logic. – Henry Harutyunyan Jan 14 '20 at 14:34
  • 1
    Unrelated, but you know that Python has a full blown `logging` packag in it's stdlib, do you ? – bruno desthuilliers Jan 14 '20 at 14:39
  • @HenryHarutyunyan If the user deletes the file a millisecond after the `os.path.exists` line has been executed, the file will be recreated anyway. It is a well known race condition. Look also at the first sentence of [this answer](https://stackoverflow.com/a/82852/1232660) with 5k upvotes. – Jeyekomon Jan 14 '20 at 14:40
  • Note the C fopen API has the behaviour you describe, but the unix create/open behaviour is not racy. I suspect the main python file API is based on C, but the low-level file APIs are also available. – Gem Taylor Jan 14 '20 at 14:41
  • @brunodesthuilliers I think it's quite related. `logging` is nice. As far as I remember there was even a way to play around between standard output and a file. I'll try to find it. – Henry Harutyunyan Jan 14 '20 at 14:42
  • @brunodesthuilliers The question is not necessarily related to the logging functionality. – Jeyekomon Jan 14 '20 at 14:43
  • @Jeyekomon that's why I said it was unrelated ;-) - I just wanted to mention it (given your snippet's file name) since quite a few people seem to try and reinvent the wheel when there's already something working in the stdlib (I once spent way too much time implementing a half-backed CSV lib before I noticed there was already a fully working one at hand). – bruno desthuilliers Jan 14 '20 at 15:03

1 Answers1

1

Use os.open and os.fdopen separately rather than open.

The "a" mode used by open tries to open the file using the flags os.O_APPEND and os.O_CREAT, creating the file if it doesn't already exist. We'll use os.fdopen to use just the os.O_APPEND flag, causing a FileNotFoundError to be raised if it doesn't already exist.

Assuming it succeeds, we'll use os.fdopen to wrap the file descriptor returned by fdopen in a file-like object. (This requires, unfortunately, a seemingly redundant "a" flag.)

import os
import sys

try:
    fd = os.open('script.log', os.O_APPEND)
    with os.fdopen(fd, "a") as f:
        f.write("foo")
except FileNotFoundError:
    print("foo")
chepner
  • 497,756
  • 71
  • 530
  • 681
  • 1
    I'll note that it would seem cleaner to either call `fdopen` in the `try` block or set `f = sys.stdout` in the `except` clause, then have a single `with` statement that uses `f`. It's not clear to me, though, how to organize this without attempting to close `sys.stdout`. A previous version of this answer tried using a `contextlib.ExitStack`, but I'm pretty sure it was not correct, so I'm leaving the simpler, though less DRY, version in its place. – chepner Jan 14 '20 at 15:18