| # Copyright (C) 2010, 2012 Google Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # * Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # * Redistributions in binary form must reproduce the above |
| # copyright notice, this list of conditions and the following disclaimer |
| # in the documentation and/or other materials provided with the |
| # distribution. |
| # * Neither the name of Google Inc. nor the names of its |
| # contributors may be used to endorse or promote products derived from |
| # this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| import logging |
| import os |
| import sys |
| import time |
| |
| LOG_HANDLER_NAME = 'MeteredStreamLogHandler' |
| |
| |
| class MeteredStream(object): |
| """ |
| This class implements a stream wrapper that has 'meters' as well as |
| regular output. A 'meter' is a single line of text that can be erased |
| and rewritten repeatedly, without producing multiple lines of output. It |
| can be used to produce effects like progress bars. |
| """ |
| |
| @staticmethod |
| def _erasure(txt): |
| num_chars = len(txt) |
| return '\b' * num_chars + ' ' * num_chars + '\b' * num_chars |
| |
| @staticmethod |
| def _ensure_newline(txt): |
| return txt if txt.endswith('\n') else txt + '\n' |
| |
| def __init__(self, stream=None, verbose=False, logger=None, time_fn=None, pid=None, number_of_columns=None, print_timestamps=None): |
| self._stream = stream or sys.stderr |
| self._verbose = verbose |
| self._print_timestamps = verbose if print_timestamps is None else print_timestamps |
| self._time_fn = time_fn or time.time |
| self._pid = pid or os.getpid() |
| self._isatty = self._stream.isatty() |
| self._erasing = self._isatty and not verbose |
| self._last_partial_line = '' |
| self._last_write_time = 0.0 |
| self._throttle_delay_in_secs = 0.066 if self._erasing else 10.0 |
| self._number_of_columns = sys.maxsize |
| if self._isatty and number_of_columns: |
| self._number_of_columns = number_of_columns |
| |
| self._logger = logger |
| self._log_handler = None |
| if self._logger: |
| log_level = logging.DEBUG if verbose else logging.INFO |
| self._log_handler = _LogHandler(self) |
| self._log_handler.setLevel(log_level) |
| self._logger.addHandler(self._log_handler) |
| |
| def __del__(self): |
| self.cleanup() |
| |
| def cleanup(self): |
| if self._logger: |
| self._logger.removeHandler(self._log_handler) |
| self._log_handler = None |
| |
| def write_throttled_update(self, txt): |
| now = self._time_fn() |
| if now - self._last_write_time >= self._throttle_delay_in_secs: |
| self.write_update(txt, now) |
| |
| def write_update(self, txt, now=None): |
| self.write(txt, now) |
| if self._erasing: |
| self._last_partial_line = txt[txt.rfind('\n') + 1:] |
| |
| def write(self, txt, now=None, pid=None): |
| now = now or self._time_fn() |
| pid = pid or self._pid |
| self._last_write_time = now |
| if self._last_partial_line: |
| self._erase_last_partial_line() |
| |
| timestamp_string = '' |
| if self._print_timestamps: |
| now_tuple = time.localtime(now) |
| timestamp_string = '%02d:%02d:%02d.%03d %d ' % (now_tuple.tm_hour, now_tuple.tm_min, now_tuple.tm_sec, int((now * 1000) % 1000), pid) |
| |
| if not self._isatty or self._verbose: |
| txt = self._ensure_newline(txt) |
| |
| try: |
| self._stream.write(timestamp_string + txt) |
| self._stream.flush() |
| except UnicodeEncodeError: |
| output = '' |
| for c in timestamp_string + txt: |
| try: |
| output += '{}'.format(c) |
| except UnicodeEncodeError: |
| output += '?' |
| self._stream.write(output) |
| self._stream.flush() |
| |
| def writeln(self, txt, now=None, pid=None): |
| self.write(self._ensure_newline(txt), now, pid) |
| |
| def _erase_last_partial_line(self): |
| num_chars = len(self._last_partial_line) |
| self._stream.write(self._erasure(self._last_partial_line)) |
| self._last_partial_line = '' |
| self._stream.flush() |
| |
| def flush(self): |
| if self._last_partial_line: |
| self._stream.write('\n') |
| self._last_partial_line = '' |
| self._stream.flush() |
| |
| def number_of_columns(self): |
| return self._number_of_columns |
| |
| |
| class _LogHandler(logging.Handler): |
| def __init__(self, meter): |
| logging.Handler.__init__(self) |
| self._meter = meter |
| self.name = LOG_HANDLER_NAME |
| |
| def emit(self, record): |
| self._meter.writeln(record.getMessage(), record.created, record.process) |