""" Classes and functions for logging robot data to a file. Includes utilities for prefixing log lines and teeing a stream to multiple destinations. """ from __future__ import annotations import sys from datetime import datetime from io import TextIOWrapper from pathlib import Path from typing import Callable, TextIO from robot_utils import get_match_data DATE_IDENTIFIER = datetime.now().strftime("%Y_%m_%dT%H_%M_%S") class Tee(TextIOWrapper): """Forwards calls from its `write` and `flush` methods to each of the given targets.""" def __init__(self, *streams: TextIO) -> None: self.streams = streams def write(self, data: str, /) -> int: """ Writes the given data to all streams in the logger. :param data: The data to be written to the stream. """ written = 0 for stream in self.streams: written = stream.write(data) self.flush() return written def flush(self) -> None: """Flushes all the streams in the logger.""" for stream in self.streams: stream.flush() class InsertPrefix(TextIOWrapper): """Inserts a prefix into the data written to the stream.""" def __init__(self, stream: TextIO, prefix: Callable[[], str] | str | None) -> None: self.stream = stream self.prefix = prefix self._line_start = True def _get_prefix(self) -> str: if not self.prefix: return '' prefix = self.prefix() if callable(self.prefix) else self.prefix return prefix def write(self, data: str, /) -> int: """ Writes the given data to the stream, applying a prefix to each line if necessary. :param data: The data to be written to the stream. """ prefix = self._get_prefix() if not prefix: return self.stream.write(data) if self._line_start: data = prefix + data self._line_start = data.endswith('\n') # Append our prefix just after all inner newlines. Don't append to a # trailing newline as we don't know if the next line in the log will be # from this zone. data = data.replace('\n', '\n' + prefix) if self._line_start: data = data[:-len(prefix)] return self.stream.write(data) def flush(self) -> None: """ Flushes the stream. This method flushes the stream to ensure that all buffered data is written to the underlying file or device. """ self.stream.flush() self.stream.flush() def prefix_and_tee_streams(name: Path, prefix: Callable[[], str] | str | None = None) -> None: """ Tee stdout and stderr also to the named log file. Note: we intentionally don't provide a way to clean up the stream replacement so that any error handling from Python which causes us to exit is also captured by the log file. """ log_file = name.open(mode='w') sys.stdout = InsertPrefix( Tee( sys.stdout, log_file, ), prefix=prefix, ) sys.stderr = InsertPrefix( Tee( sys.stderr, log_file, ), prefix=prefix, ) def get_match_identifier() -> str: """ Get the identifier for this run of the simulator. This identifier is used to name the log files. :return: The match identifier """ match_data = get_match_data() if match_data.match_number is not None: return f"match-{match_data.match_number}" else: return DATE_IDENTIFIER