Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python context for file or None

Python is going to call a subprocess, the user either requested that subprocesses stdout is to go to a file (or back-holed to os.devnull), or the subprocesses output is to be passed though "in real time".

My current best guess as how to do this would seemingly work:

  • Let file_path be valid input for open()
  • Let logging be a Boolean indicator, true indicating use file_path for logging or false to passthough to stdout.

with open(file_path, 'wb') if logging else None as shell_stdout:
    subprocess.call(['ls'], stdout=shell_stdout)

In tinkering/testing this appears to be the right value, which I have assumed work well with subprocess.call. However, and unsurprisingly I get the following exception:

AttributeError: __exit__

So None is not a context, it has no __exit__;


Goals

  • Not open a file at all if the user does not want logging.
  • Use contexts (as provided by the stdlib), (Preference; I can't imagine doing the file open/close operations manually being any cleaner.)
  • Not need a try/catch (Preference to avoid further nesting)
  • Only have a single call to subprocesses.call (Non duplicated line)

So, how could this behavior be achieved? Or what would you suggest doing instead/alternatively?

like image 840
ThorSummoner Avatar asked Dec 24 '22 22:12

ThorSummoner


2 Answers

Either set shell_stdout to stdout or a file object based on logging being True or False, there is no need to overcomplicate it, you only have one condition or the other if logging will either be True or False, there is nothing wrong with opening a file not using with, there are times when using with does not fit.

import sys

if logging:
      shell_stdout = open(file_path, 'wb') # to file or devnull if logging
else:
    shell_stdout = sys.stdout
subprocess.call(['ls'], stdout=shell_stdout) # real time if not logging
shell_stdout.close()

To do what you wanted in your question you can do the following, you don't need anything bar sys.stdout:

with open(file_path, 'wb') if logging else sys.stdout as shell_stdout:
    subprocess.call(['ls'], stdout=shell_stdout)

The file will only be created if logging is True.

like image 37
Padraic Cunningham Avatar answered Jan 02 '23 12:01

Padraic Cunningham


You could create a "no-op" context manager:

import subprocess
import contextlib
@contextlib.contextmanager
def noop():
    yield None

logging = False
file_path = '/tmp/out'

with open(file_path, 'wb') if logging else noop() as shell_stdout:
    subprocess.call(['ls'], stdout=shell_stdout)

When logging is True, the conditional expression returns a file object. When logging is False, it returns a noop() context manager (so it can be used in the with-statement), which sets shell_out to None but doesn't do anything special upon exit.


Per the docs, when stdout=None,

... no redirection will occur; the child’s file handles will be inherited from the parent.

Usually the parent's stdout would equal sys.stdout. However, it is possible to redirect sys.stdout somewhere else, either explicitly (e.g. sys.stdout = open('/tmp/stdout', 'wb')) or indirectly, such as by using module that redirects sys.stdout. The fileinput module from the standard library redirects sys.stdout, for example. In such cases noop() might be useful for directing stdout to the parent's stdout, which may be different than sys.stdout.

If this corner case does not affect you, then Padraic Cunningham's solution is simpler:

with open(file_path, 'wb') if logging else sys.stdout as shell_stdout:
    subprocess.call(['ls'], stdout=shell_stdout)
like image 149
unutbu Avatar answered Jan 02 '23 11:01

unutbu