0

I have tried my hand at writing a Python context manager that safely opens a file for reading and gracefully deals with a FileNotFound error. Here's my code:

filename = 'my_file.txt'

class SafeRead:

    def __init__(self,fname):
        self.filename = fname

    def __enter__(self):
        try:
            self.file_handle = open(self.filename,'r')
        except Exception as e:
            self.file_handle = None
            print(e)
        return self.file_handle

    def __exit__(self,e_type,e_val,e_trace):
        if self.file_handle:
            self.file_handle.close()

with SafeRead(filename) as f:
    if f: data = f.read()

Is it possible to write a context manager that suppresses the execution of the inner block obviating the need for the additional check on the file handle?

BeMuSeD
  • 181
  • 2
  • 8
  • 1
    Does it work? [codereview.se]. – 001 Jan 27 '23 at 15:53
  • 1
    There's nothing graceful about how you are handling this exception. You've just replaced the need to handle the exception with the need to check the value of `f`. This is *less* safe than before. – chepner Jan 27 '23 at 15:54
  • A truly "safe" read would supply a suitable file-like object in place of `None`. Perhaps `f.read()` returns an empty string, but maybe would have been true of `my_file.txt` as well. – chepner Jan 27 '23 at 15:56
  • thanks @chepner was missing the if f: clause and wasnt able to figure out how it could work : You've just replaced the need to handle the exception with the need to check the value of f. This is less safe than before – pippo1980 Jan 27 '23 at 16:19
  • I realise the word "safe" is misplaced here. Basically I am asking if it is possible to have the context manager handle both the f.close() and the exception handling "in the background". – BeMuSeD Jan 29 '23 at 15:01

2 Answers2

1

Your code can be simplified using the contextlib.contextmanager decorator.

from contextlib import contextmanager

@contextmanager
def SafeRead(filename):
    try:
        file_handle = open(filename, 'r')
        yield file_handle
        file_handle.close()
    except FileNotFoundError as e:
        print(e)
        yield None

filename = 'test.txt'

with SafeRead(filename) as f:
    if f: data = f.read()

As for not having to check the value of the returned file handle, I don't think it can be done without resorting to black magic.

Fractalism
  • 1,231
  • 3
  • 12
0

"Safe" in this context usually means that even if you can't return what you intended to return, you can return something similar enough that your code can continue in either case. The simplest thing you could do is return a file-like object that you can still read from. What you read from that object depends on the situation, so you may as well let the caller provide a suitable value in case the file it inaccessible. For example,

from io import StringIO

class SafeRead:

    def __init__(self, fname, data=""):
        self.filename = fname
        self.backup = data

    def __enter__(self):
        try:
            self.file_handle = open(self.filename,'r')
        except Exception as e:
            self.file_handle = StringIO(self.backup)
        return self.file_handle

    def __exit__(self,e_type,e_val,e_trace):
        self.file_handle.close()


with SafeRead(filename, "foobar") as f:
    assert os.path.exists(filename) or f.read() == "foobar"
chepner
  • 497,756
  • 71
  • 530
  • 681