How to write a context manager wrapper for "open" that satisfies Pylint's consider-using-with

Viewed 281

I have a dilemma writing a context/resource manager wrapper for a file object which passes Pylint, for use with with: Do I put the wrapped open call in __init__, or in __enter__?

Conceptually, this is a class that wraps open by accepting either a filename, the string -, or a file object (such as sys.stdin) and does 'the right thing'. In other words, if it's a filename other than -, it opens the file and manages it as a resource; otherwise, it either chooses a default file object (expected to be sys.stdin or sys.stdout) if the filename is -, or if it is a file object, uses that file object unchanged.

I see two possibilities, neither of which are working out right now: The wrapped open goes in the __init__ constructor, or it goes in the __enter__ context management method. If I put it in the __init__ constructor, which examples on SO suggest, Pylint -- which I must pass -- fails on the open with R1732: Consider using 'with' for resource-allocating operations (consider-using-with). If I put the open in the __enter__ method, Pylint is happy, but I am not sure this is the correct practice, plus the problem is, in one use case, I need the file object in order to initialize a base class (and Pylint won't let me call the base class constructor in the __enter__ method).

Some example code is in order. Here is code that opens in the constructor:

class ManagedFile:
    '''Manage a file, which could be an unopened filename, could be
    '-' for stdin/stdout, or could be an existing filehandle'''
    def __init__(self, file_in, handle_default, open_kwargs):
        ''' Open a file if given a filename, or handle_default if -,
        or if it's a file object already, just pass it through.
        :param file_in: Valid filename, '-', or file-like object
        :param handle_default: What to return if file is '-'
        :param open_kwargs: Dictionary of options to pass to open() if used
        '''
        self.file_handle = None
        self.file_in = None
        if isinstance(file_in, io.IOBase):
            self.file_handle = file_in
        elif isinstance(file_in, str):
            if file_in is None or file_in == "-":
                self.file_handle = handle_default
            else:
                self.file_handle = open(self.file_in, **open_kwargs)
                self.file_in = file_in
        else:
            raise TypeError('File specified must be string or file object')

    def __enter__(self):
        self.file_handle.__enter__()
        return self.file_handle

    def __exit__(self, err_type, err_value, traceback):
        self.file_handle.__exit__(err_type, err_value, traceback)
        self.file_handle.close()
        self.file_in = None

    def handle(self):
        '''Return handle of file that was opened'''
        return self.file_handle

And here is how I would do it with the open call in the __enter__ method:

class ManagedFile:
    '''Manage a file, which could be an unopened filename, could be
    '-' for stdin/stdout, or could be an existing filehandle'''
    def __init__(self, file_in, handle_default, open_kwargs):
        ''' Open a file if given a filename, or handle_default if -,
        or if it's a file object already, just pass it through.
        :param file_in: Valid filename, '-', or file-like object
        :param handle_default: What to return if file is '-'
        :param open_kwargs: Dictionary of options to pass to open() if used
        :return: Managed file object
        '''
        self.managed = False
        self.file_handle = None
        self.open_kwargs = {'mode': 'r'}
        self.file_in = None
        if isinstance(file_in, io.IOBase):
            self.file_handle = file_in
        elif isinstance(file_in, str):
            if file_in is None or file_in == "-":
                self.file_handle = handle_default
            else:
                self.file_in = file_in
                self.open_kwargs = open_kwargs
                self.managed = True
        else:
            raise TypeError('File specified must be string or file object')

    def __enter__(self):
        if self.managed:
            self.file_handle = open(self.file_in, **self.open_kwargs)
        self.file_handle.__enter__()
        return self.file_handle

    def __exit__(self, err_type, err_value, traceback):
        self.file_handle.__exit__(err_type, err_value, traceback)
        self.file_handle.close()
        self.managed = False
        self.file_in = None

    def handle(self):
        '''Return handle of file that was opened'''
        return self.file_handle

I've pored over many SO questions & answers, but haven't been able to triangulate the exact answer I'm looking for, particular one that accounts for why Pylint flags an error when I ostensibly do the right thing.

Undoubtedly I've committed some Python errors in this code so any other ancillary correction would be welcome. Other ideas are also welcome but please don't get too fancy on me.

Ideally the class would itself behave as a full-fledged file object, but right now I'm focusing on something simple: Something that just manages a file handle (i.e. a reference to a regular file object). Extra gratitude if someone can provide some hints on turning it into a file object.

Python version is 3.8.10; platform is Linux.

1 Answers

If you don't need this to be a class, this feels like it's a lot simpler.

import contextlib
import io
import sys

def open_or_stdout(file_or_path, mode):
    """
    Returns a context manager with either the file path opened,
    the file object passed through or standard output

    """
    if isinstance(file_or_path, io.IOBase):
        # Input is already a file-like object. Just pass it through
        return file_or_path

    if file_or_path == "-":
        return contextlib.nullcontext(sys.stdout)

    return open(path, mode=mode) 

If you want to stick with the class, I think you need to return the value of self.file_handle.__enter__() in your implementation of __enter__() rather than self.file_handle.

Related