Skip to content

Instantly share code, notes, and snippets.

@grynko
Last active March 16, 2021 15:10
Show Gist options
  • Select an option

  • Save grynko/a4370d7d2dd843a1afc19e6e4ea9e15c to your computer and use it in GitHub Desktop.

Select an option

Save grynko/a4370d7d2dd843a1afc19e6e4ea9e15c to your computer and use it in GitHub Desktop.
Python wrapper of tee - read from given input file descriptors and write to standard output and files.
# The MIT License (MIT)
#
# Copyright (c) 2021 Ivan Grynko
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
import sys
import subprocess
import logging
import functools
from typing import Iterable
class Tee:
"""
Tee - read from given inputs and write to standard output and files.
"""
def __init__(self, in_files: Iterable[int], out_files: Iterable[str], mute: bool = False):
"""
Args:
in_files: input file descriptors.
out_files: output file names.
mute: whether to mute duplication to stdout. Defaults to False.
"""
if not in_files:
raise ValueError('At least one input file descriptor must be specified.')
self.in_files = in_files
self.out_files = out_files
self.mute = mute
@property
def logger(self):
return logging.getLogger('Tee')
@property
def stdin(self):
if self.tee:
return self.tee.stdin
def start(self):
"""Start capturing input files."""
self.tee = subprocess.Popen(
["tee", *self.out_files],
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL if self.mute else None
)
self._saved_fds = []
for fd in self.in_files:
self._saved_fds.append(os.dup(fd))
os.dup2(self.tee.stdin.fileno(), fd)
def stop(self, timeout=5):
"""Stop capturing input files.
Args:
timeout: seconds to wait subprocess to exit gracefully.
"""
if self.tee:
self.tee.stdin.close()
# close all fds
for fd_src, fd_dst in zip(self._saved_fds, self.in_files):
os.dup2(fd_src, fd_dst)
os.close(fd_src)
# terminate tee
try:
self.tee.wait(timeout=timeout)
except subprocess.TimeoutExpired:
self.logger.warning("tee process didn't quit gracefully, terminating...")
self.tee.terminate()
self.tee = None
def __enter__(self, *args, **kwargs):
self.start()
return self
def __exit__(self, *args, **kwargs):
self.stop()
def tee(in_files: Iterable[int], out_files: Iterable[str], mute: bool = False):
"""Tee decorator.
Typical usage:
@tee(in_files=[sys.stdout.fileno()], out_files=['output.log'])
def func():
print('I need the line in stdout as well as in output.log')
"""
def factory(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
with Tee(in_files, out_files, mute):
return fn(*args, **kwargs)
return wrapper
return factory
tee_stdout = functools.partial(tee, (sys.stdout.fileno(),))
tee_stderr = functools.partial(tee, (sys.stderr.fileno(),))
tee_stdall = functools.partial(tee, (sys.stdout.fileno(), sys.stderr.fileno(),))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment