Source code for stouputils.ctx

"""
This module provides context managers for temporarily silencing output.

- Muffle: Context manager that temporarily silences output (alternative to stouputils.decorators.silent())
- LogToFile: Context manager to log to a file every print call (with LINE_UP handling)

.. image:: https://raw.githubusercontent.com/Stoupy51/stouputils/refs/heads/main/assets/ctx_module.gif
  :alt: stouputils ctx examples
"""

# Imports
from __future__ import annotations
import os
import sys
import time
from collections.abc import Callable
from typing import IO, Any, TextIO

from .io import super_open
from .print import TeeMultiOutput, debug


# Context manager to temporarily silence output
[docs] class Muffle: """ Context manager that temporarily silences output. Alternative to stouputils.decorators.silent() Examples: >>> with Muffle(): ... print("This will not be printed") """ def __init__(self, mute_stderr: bool = False) -> None: self.mute_stderr: bool = mute_stderr """ Attribute remembering if stderr should be muted """ self.original_stdout: TextIO = sys.stdout """ Attribute remembering original stdout """ self.original_stderr: TextIO = sys.stderr """ Attribute remembering original stderr """ def __enter__(self) -> Muffle: """ Enter context manager which redirects stdout and stderr to devnull """ # Redirect stdout to devnull sys.stdout = open(os.devnull, "w", encoding="utf-8") # Redirect stderr to devnull if needed if self.mute_stderr: sys.stderr = open(os.devnull, "w", encoding="utf-8") # Return self return self def __exit__(self, exc_type: type[BaseException]|None, exc_val: BaseException|None, exc_tb: Any|None) -> None: """ Exit context manager which restores original stdout and stderr """ # Restore original stdout sys.stdout.close() sys.stdout = self.original_stdout # Restore original stderr if needed if self.mute_stderr: sys.stderr.close() sys.stderr = self.original_stderr
# Context manager to log to a file
[docs] class LogToFile: """ Context manager to log to a file. This context manager allows you to temporarily log output to a file while still printing normally. The file will receive log messages without ANSI color codes. Args: path (str): Path to the log file mode (str): Mode to open the file in (default: "w") encoding (str): Encoding to use for the file (default: "utf-8") tee_stdout (bool): Whether to redirect stdout to the file (default: True) tee_stderr (bool): Whether to redirect stderr to the file (default: True) ignore_lineup (bool): Whether to ignore lines containing LINE_UP escape sequence in files (default: False) Examples: .. code-block:: python > import stouputils as stp > with stp.LogToFile("output.log"): > stp.info("This will be logged to output.log and printed normally") > print("This will also be logged") """ def __init__( self, path: str, mode: str = "w", encoding: str = "utf-8", tee_stdout: bool = True, tee_stderr: bool = True, ignore_lineup: bool = True ) -> None: self.path: str = path """ Attribute remembering path to the log file """ self.mode: str = mode """ Attribute remembering mode to open the file in """ self.encoding: str = encoding """ Attribute remembering encoding to use for the file """ self.tee_stdout: bool = tee_stdout """ Whether to redirect stdout to the file """ self.tee_stderr: bool = tee_stderr """ Whether to redirect stderr to the file """ self.ignore_lineup: bool = ignore_lineup """ Whether to ignore lines containing LINE_UP escape sequence in files """ self.file: IO[Any] = super_open(self.path, mode=self.mode, encoding=self.encoding) """ Attribute remembering opened file """ self.original_stdout: TextIO = sys.stdout """ Original stdout before redirection """ self.original_stderr: TextIO = sys.stderr """ Original stderr before redirection """ def __enter__(self) -> LogToFile: """ Enter context manager which opens the log file and redirects stdout/stderr """ # Redirect stdout and stderr if requested if self.tee_stdout: sys.stdout = TeeMultiOutput(self.original_stdout, self.file, ignore_lineup=self.ignore_lineup) if self.tee_stderr: sys.stderr = TeeMultiOutput(self.original_stderr, self.file, ignore_lineup=self.ignore_lineup) # Return self return self def __exit__(self, exc_type: type[BaseException]|None, exc_val: BaseException|None, exc_tb: Any|None) -> None: """ Exit context manager which closes the log file and restores stdout/stderr """ # Restore original stdout and stderr if self.tee_stdout: sys.stdout = self.original_stdout if self.tee_stderr: sys.stderr = self.original_stderr # Close file self.file.close()
[docs] @staticmethod def common(logs_folder: str, filepath: str, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any: """ Common code used at the beginning of a program to launch main function Args: logs_folder (str): Folder to store logs in filepath (str): Path to the main function func (Callable[..., Any]): Main function to launch *args (tuple[Any, ...]): Arguments to pass to the main function **kwargs (dict[str, Any]): Keyword arguments to pass to the main function Returns: Any: Return value of the main function Examples: >>> if __name__ == "__main__": ... LogToFile.common(f"{ROOT}/logs", __file__, main) """ # Import datetime from datetime import datetime # Build log file path file_basename: str = os.path.splitext(os.path.basename(filepath))[0] date_time: str = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") date_str, time_str = date_time.split("_") log_filepath: str = f"{logs_folder}/{file_basename}/{date_str}/{time_str}.log" # Launch function with arguments if any with LogToFile(log_filepath): return func(*args, **kwargs)
# Context manager to measure execution time
[docs] class MeasureTime: """ Context manager to measure execution time. This context manager measures the execution time of the code block it wraps and prints the result using a specified print function. Args: print_func (Callable): Function to use to print the execution time (e.g. debug, info, warning, error, etc.). message (str): Message to display with the execution time. Defaults to "Execution time". perf_counter (bool): Whether to use time.perf_counter_ns or time.time_ns. Defaults to True. Examples: .. code-block:: python > import time > import stouputils as stp > with stp.MeasureTime(stp.info, message="My operation"): ... time.sleep(0.5) > # [INFO HH:MM:SS] My operation: 500.123ms (500123456ns) > with stp.MeasureTime(): # Uses debug by default ... time.sleep(0.1) > # [DEBUG HH:MM:SS] Execution time: 100.456ms (100456789ns) """ def __init__( self, print_func: Callable[..., None] = debug, message: str = "Execution time", perf_counter: bool = True ) -> None: self.print_func: Callable[..., None] = print_func """ Function to use for printing the execution time """ self.message: str = message """ Message to display with the execution time """ self.perf_counter: bool = perf_counter """ Whether to use time.perf_counter_ns or time.time_ns """ self.ns: Callable[[], int] = time.perf_counter_ns if perf_counter else time.time_ns """ Time function to use """ self.start_ns: int = 0 """ Start time in nanoseconds """ def __enter__(self) -> MeasureTime: """ Enter context manager, record start time """ self.start_ns = self.ns() return self def __exit__(self, exc_type: type[BaseException]|None, exc_val: BaseException|None, exc_tb: Any|None) -> None: """ Exit context manager, calculate duration and print """ # Measure the execution time (nanoseconds and seconds) total_ns: int = self.ns() - self.start_ns total_ms: float = total_ns / 1_000_000 total_s: float = total_ns / 1_000_000_000 # Print the execution time (nanoseconds if less than 0.1s, seconds otherwise) if total_ms < 100: self.print_func(f"{self.message}: {total_ms:.3f}ms ({total_ns}ns)") elif total_s < 60: self.print_func(f"{self.message}: {(total_s):.5f}s") else: minutes: int = int(total_s) // 60 seconds: int = int(total_s) % 60 if minutes < 60: self.print_func(f"{self.message}: {minutes}m {seconds}s") else: hours: int = minutes // 60 minutes: int = minutes % 60 if hours < 24: self.print_func(f"{self.message}: {hours}h {minutes}m {seconds}s") else: days: int = hours // 24 hours: int = hours % 24 self.print_func(f"{self.message}: {days}d {hours}h {minutes}m {seconds}s")