Source code for giant.utilities.tee

# Copyright 2021 United States Government as represented by the Administrator of the National Aeronautics and Space
# Administration.  No copyright is claimed in the United States under Title 17, U.S. Code. All Other Rights Reserved.


"""
This module provides 2 classes for performing tees of outputs to a file.

A tee (in unix terminology) refers to the ability to pipe 1 input to multiple outputs.  Here that means we can pipe a
single call to stderr or stdout to write to both the standard stderr/stdout, as well as a user specified file.  This is
useful for logging an interactive program (where you want to print both to the screen and to a file).

The :class:`Tee` class is used to tee either STDOUT or STDERR to a file, while the :class:`DoubleTee` class is used to
tee both STDOUT and STDERR to the same file.  Both of these classes can be used as context managers.
"""


import sys

from typing import Union, IO, Optional

from enum import Enum

import os

from io import IOBase

from .._typing import PATH


[docs]class REDIRECT(Enum): """ An enumeration specifying the options of what to redirect """ STDOUT = 'stdout' """ Redirect STDOUT """ STDERR = 'stderr' """ Redirect STDERR """
[docs]class Tee: r""" This class support simultaneously writing to a file and stdout/stderr Essentially, all this does is serially call the write method for the original stdout/stderr object followed by the write method for the file being teed to. This is also a context manager so you can use it with a ``with`` block for automatic opening/closing. If not using in a with block, you should call :meth:`close` when done to set things back to normal. Example: >>> from giant.utilities.tee import Tee, REDIRECT >>> with Tee('mylog.txt', redirect=REDIRECT.STDOUT): ... print('first line') ... print('Second line') ... print('I like tacos!!') first line second line I like tacos!! >>> with open('mylog.txt', 'r') as ifile: ... print(ifile.readlines()) ['first line\n', 'second line\n', 'I like tacos!!\n'] Non-context manager example: >>> from giant.utilities.tee import Tee, REDIRECT >>> mytee = Tee('mylog.txt', redirect=REDIRECT.STDOUT) >>> print('first line') first line >>> print('second line') second line >>> print('I like tacos!!') I like tacos!! >>> mytee.close() >>> with open('mylog.txt', 'r') as ifile: ... print(ifile.readlines()) ['first line\n', 'second line\n', 'I like tacos!!\n'] inspiration: https://stackoverflow.com/a/24583265/3431189 """ def __init__(self, file: Union[PATH, IO], redirect: Union[REDIRECT, str] = REDIRECT.STDOUT, mode: str = "a+", buff: int = -1): """ :param file: The file to write to as a string or a ``IO`` object which defines write, flush, and close methods. :param redirect: Whether to redirect STDOUT or STDERR to the file. :param mode: The mode to open the file with. Should be either 'w', 'a+', or 'r+'. Ignored if ``file`` is ``IO`` :param buff: How to buffer the file. Should be -1 (for system default) or >= 1. Ignored if ``file`` is ``IO`` """ # grab the originals self.stdout = None # type: Optional[IO] """ A copy of the original STDOUT when ``redirect`` is set to ``STDOUT`` and this is open, otherwise ``None`` """ self.stderr = None # type: Optional[IO] """ A copy of the original STDERR when ``redirect`` is set to ``STDERR`` and this is closed, otherwise ``None`` """ self.file = None # type: Optional[IO] """ The file object to tee to or ``None`` if this is closed. """ if isinstance(file, IOBase): self.file = file else: self.file = open(file, mode=mode, buffering=buff) self.redirect = redirect if isinstance(redirect, REDIRECT) else REDIRECT(redirect.lower()) # type: REDIRECT """ Specifies whether to tee STDOUT or STDERR to the file. """ # store the appropriate IO object and replace it if self.redirect == REDIRECT.STDOUT: self.stdout = sys.stdout sys.stdout = self elif self.redirect == REDIRECT.STDERR: self.stderr = sys.stderr sys.stderr = self def __repr__(self) -> str: return 'Tee({}, redirect={})'.format(repr(self.file), self.redirect) def __str__(self) -> str: if self.file is None: if self.redirect == REDIRECT.STDOUT: return "Closed Tee for STDOUT" else: return "Closed Tee for STDERR" else: if self.redirect == REDIRECT.STDOUT: return "Open Tee for STDOUT to {}".format(self.file.name) else: return "Open Tee for STDERR to {}".format(self.file.name) def __del__(self): self.close() def __enter__(self): pass def __exit__(self, *args): self.close()
[docs] def write(self, message: str) -> None: """ This method dispatches to both the file write method and stdout or stderr depending on what was selected at initialization for the ``redirect`` parameter. :param message: The message to print :raises ValueError: If close method was called prior to this call """ if self.redirect == REDIRECT.STDOUT: if self.stdout is None: raise ValueError('Close method already called') else: self.stdout.write(message) elif self.redirect == REDIRECT.STDERR: if self.stderr is None: raise ValueError('Close method already called') else: self.stderr.write(message) if self.file is None: raise ValueError('Close method already called') else: self.file.write(message)
[docs] def flush(self) -> None: """ This method flushes all data by calling both the file flush method and stdout or stderr's flush method depending on what was selected at initialization for the ``redirect`` parameter. :raises ValueError: If close method was called prior to this call """ if self.file is None: raise ValueError('Close method already called') else: if self.redirect == REDIRECT.STDOUT: self.stdout.flush() elif self.redirect == REDIRECT.STDERR: self.stderr.flush() self.file.flush() os.fsync(self.file.fileno())
[docs] def close(self) -> None: """ This method closes the file object and resets stdout/stderr to their values before creating an instance of this class. It is safe to call this multiple times, however, all subsequent calls will do nothing. """ # reset STDOUT to what it was if self.stdout is not None: sys.stdout = self.stdout self.stdout = None # reset STDERR to what it was if self.stderr is not None: sys.stderr = self.stderr self.stderr = None # close file if self.file is not None: self.file.close() self.file = None
[docs]class DoubleTee: r""" This class provides the ability to simultaneously tee both STDOUT and STDERR to a file. This is done by creating two instances of the :class:`Tee` class, one for STDOUT and STDERR. This class can be used as a context manager, which will automatically handle closing the :class:`Tee` instances for you. If you do not use it as a context manager, you should call :meth:`close` manually when you are done. Example: >>> from giant.utilities.tee import DoubleTee >>> import sys >>> with DoubleTee('mylog.txt'): ... print('first line') ... print('Second line') ... print('I like tacos!!', file=sys.stderr) first line second line I like tacos!! >>> with open('mylog.txt', 'r') as ifile: ... print(ifile.readlines()) ['first line\n', 'second line\n', 'I like tacos!!\n'] Non-context manager example: >>> from giant.utilities.tee import DoubleTee >>> import sys >>> doubletee = DoubleTee('mylog.txt') >>> print('first line') first line >>> print('second line') second line >>> print('I like tacos!!', file=sys.stderr) I like tacos!! >>> doubletee.close() >>> with open('mylog.txt', 'r') as ifile: ... print(ifile.readlines()) ['first line\n', 'second line\n', 'I like tacos!!\n'] """ def __init__(self, file: Union[PATH, IO], mode: str = "a+", buff: int = -1): """ :param file: The file to write to as a string or a ``IO`` object which defines write, flush, and close methods. :param mode: The mode to open the file with. Should be either 'w', 'a', or 'r+'. Ignored if ``file`` is ``IO`` :param buff: How to buffer the file. Should be -1 (for system default) or >= 1. Ignored if ``file`` is ``IO`` """ if isinstance(file, IOBase): tfile = file else: tfile = open(file, mode=mode, buffering=buff) self.stdout = Tee(tfile, redirect=REDIRECT.STDOUT) # type: Tee """ An instance of Tee for teeing stdout to the requested file """ self.stderr = Tee(tfile, redirect=REDIRECT.STDERR) # type: Tee """ An instance of Tee for teeing stderr to the requested file """ def __repr__(self) -> str: return 'DoubleTee({})'.format(repr(self.stdout.file)) def __str__(self) -> str: if self.stdout.file is None: return "Closed DoubleTee" else: return "Open DoubleTee to {}".format(self.stdout.file.name) def __del__(self) -> None: """ Deleter calls :meth:`close` in case it wasn't already called. """ self.close() def __enter__(self) -> None: """ Everything needed for entry is handled in ``__init__`` so does nothing """ pass def __exit__(self, *args) -> None: """ Dispatches to :meth:`close` """ self.close()
[docs] def close(self) -> None: """ Closes both the STDOUT and STDERR Tees. """ self.stdout.close() self.stderr.close()