Source code for stouputils.print.output_stream


# Imports
from typing import IO, Any

from ..config import StouputilsConfig as Cfg
from .utils import remove_colors


# TeeMultiOutput class to duplicate output to multiple file-like objects
[docs] class TeeMultiOutput: """ File-like object that duplicates output to multiple file-like objects. Args: *files (IO[Any]): One or more file-like objects that have write and flush methods strip_colors (bool): Strip ANSI color codes from output sent to non-stdout/stderr files ascii_only (bool): Replace non-ASCII characters with their ASCII equivalents for non-stdout/stderr files ignore_lineup (bool): Ignore lines containing LINE_UP escape sequence in non-terminal outputs Examples: >>> import sys >>> f = open("logfile.txt", "w") >>> sys.stdout = TeeMultiOutput(sys.stdout, f) >>> print("Hello World") # Output goes to both console and file Hello World >>> f.close() # TeeMultiOutput will handle any future writes to closed files gracefully """ def __init__( self, *files: IO[Any], strip_colors: bool = True, ascii_only: bool = True, ignore_lineup: bool = True ) -> None: # Flatten any TeeMultiOutput instances in files flattened_files: list[IO[Any]] = [] for file in files: if isinstance(file, TeeMultiOutput): flattened_files.extend(file.files) else: flattened_files.append(file) self.files: tuple[IO[Any], ...] = tuple(flattened_files) """ File-like objects to write to """ self.strip_colors: bool = strip_colors """ Whether to strip ANSI color codes from output sent to non-stdout/stderr files """ self.ascii_only: bool = ascii_only """ Whether to replace non-ASCII characters with their ASCII equivalents for non-stdout/stderr files """ self.ignore_lineup: bool = ignore_lineup """ Whether to ignore lines containing LINE_UP escape sequence in non-terminal outputs """ @property def encoding(self) -> str: """ Get the encoding of the first file, or "utf-8" as fallback. Returns: str: The encoding, ex: "utf-8", "ascii", "latin1", etc. """ try: return self.files[0].encoding # type: ignore except (IndexError, AttributeError): return "utf-8"
[docs] def write(self, obj: str) -> int: """ Write the object to all files while stripping colors if needed. Args: obj (str): String to write Returns: int: Number of characters written to the first file """ files_to_remove: list[IO[Any]] = [] num_chars_written: int = 0 for i, f in enumerate(self.files): try: # Check if file is closed if hasattr(f, "closed") and f.closed: files_to_remove.append(f) continue # Strip colors if needed content: str = obj if not self.strip_colors else remove_colors(obj) # Check if this file is a terminal/console or a regular file if not (hasattr(f, "isatty") and f.isatty()): # Non-terminal files get processed content # Skip content if it contains LINE_UP and ignore_lineup is True if self.ignore_lineup and (Cfg.LINE_UP in content or "\r" in content): continue # Replace Unicode block characters with ASCII equivalents # Replace other problematic Unicode characters as needed if self.ascii_only: content = content.replace('█', '#') content = ''.join(c if ord(c) < 128 else '?' for c in content) # Write content to file if i == 0: num_chars_written = f.write(content) else: f.write(content) except ValueError: # ValueError is raised when writing to a closed file files_to_remove.append(f) except Exception: pass # Remove closed files from the list if files_to_remove: self.files = tuple(f for f in self.files if f not in files_to_remove) return num_chars_written
[docs] def flush(self) -> None: """ Flush all files. """ for f in self.files: try: f.flush() except Exception: pass
[docs] def fileno(self) -> int: """ Return the file descriptor of the first file. """ return self.files[0].fileno() if hasattr(self.files[0], "fileno") else 0