55import sys
66import time
77import typing
8+ from contextlib import contextmanager
89from contextvars import ContextVar , copy_context
910from functools import partial
1011from gzip import GzipFile
2526 "RotatingFileWriter" ,
2627 "SizedTimedRotatingFileHandler" ,
2728 "ContextFilter" ,
29+ "LoggerStream" ,
2830]
2931
3032
@@ -72,9 +74,7 @@ class LogHelper:
7274 logger.info("This is an info message")
7375 """
7476
75- DEFAULT_FORMAT = (
76- "%(asctime)s | %(name)s | %(levelname)-5s | %(filename)-8s(%(lineno)s) - %(message)s"
77- )
77+ DEFAULT_FORMAT = "%(asctime)s | %(name)s | %(levelname)-5s | %(filename)-8s(%(lineno)s) - %(message)s"
7878 DEFAULT_FORMATTER = logging .Formatter (DEFAULT_FORMAT )
7979 FILENAME_HANDLER_MAP : Dict [str , logging .Handler ] = {}
8080
@@ -819,6 +819,211 @@ def filter(self, record):
819819 return True
820820
821821
822+ class LoggerStream :
823+ r"""LoggerStream constructor.
824+
825+ Args:
826+ skip_same_head (bool, optional): Whether to skip the same log head. Defaults to True.
827+ True:
828+ 24-08-10 19:30:07 This is a log message.
829+ This is another log message.
830+ 24-08-10 19:30:08 This is a new log message.
831+ False:
832+ 24-08-10 19:30:07 This is a log message.
833+ 24-08-10 19:30:07 This is another log message.
834+
835+ Example::
836+
837+ # 1. Basic usage
838+ logger = LoggerStream(skip_same_head=True)
839+ logger.write("This is a log message.\n")
840+ logger.write("This is another log message.\n")
841+ logger.write("This is a new log message.\n")
842+
843+ # 2. Redirect sys.stdout
844+ import sys
845+ sys.stdout = LoggerStream(skip_same_head=False)
846+ print("This is a log message.")
847+ print("This is a log message.")
848+ # 24-08-10 19:30:07 This is a log message.
849+ # 24-08-10 19:30:07 This is a log message.
850+
851+ # 3. Overwrite built-in print function
852+ LoggerStream.install_print()
853+ print(123)
854+ print(123)
855+ # 24-08-10 19:30:07 123
856+ # 123
857+ LoggerStream.restore_print()
858+ LoggerStream.install_print(writer=open("log.txt", "a").write)
859+
860+ # 4. Subclass and override writer method
861+ class CustomLoggerStream(LoggerStream):
862+ def __init__(self, skip_same_head=True):
863+ super().__init__(skip_same_head=skip_same_head)
864+ self.logger = setup_your_logger_somehow()
865+
866+ def writer(self, msg: str):
867+ # Custom implementation to write log message
868+ self.logger.info(msg)
869+ """
870+
871+ BUILTINS_STDOUT = sys .stdout
872+ BUILTINS_STDERR = sys .stderr
873+ BUILTINS_PRINT = print
874+
875+ def __init__ (self , skip_same_head = True , writer = None , flush = None , get_prefix = None ):
876+ self .skip_same_head = skip_same_head
877+ self .writer = writer or self .default_writer
878+ self .flush = flush or self .default_flush
879+ self .get_prefix = get_prefix or self .default_get_prefix
880+ self ._is_newline = True
881+ self ._last_head = ""
882+
883+ def default_get_prefix (self ):
884+ return time .strftime ("%y-%m-%d %H:%M:%S " )
885+
886+ @classmethod
887+ def default_writer (cls , msg : str ):
888+ cls .BUILTINS_STDOUT .write (msg )
889+ cls .BUILTINS_STDOUT .flush ()
890+
891+ def write (self , buf ):
892+ if self ._is_newline :
893+ head = self .get_prefix ()
894+ if self .skip_same_head :
895+ if head == self ._last_head :
896+ head = ""
897+ else :
898+ self ._last_head = head
899+ else :
900+ head = ""
901+ if buf .endswith ("\n " ):
902+ self ._is_newline = True
903+ else :
904+ self ._is_newline = False
905+ if head :
906+ msg = f"{ head } { buf } "
907+ else :
908+ msg = buf
909+ self .writer (msg )
910+
911+ def default_flush (self ):
912+ pass
913+
914+ @classmethod
915+ @contextmanager
916+ def replace_print_ctx (
917+ cls ,
918+ skip_same_head = True ,
919+ writer = BUILTINS_STDERR .write ,
920+ flush = BUILTINS_STDERR .flush ,
921+ get_prefix = None ,
922+ ):
923+ """Context manager to replace the built-in print function temporarily.
924+
925+ Args:
926+ skip_same_head (bool, optional): Whether to skip the same log head. Defaults to True.
927+ writer (_type_, optional): Writer function. Defaults to BUILTINS_STDERR.write.
928+ flush (_type_, optional): Flush function. Defaults to BUILTINS_STDERR.flush.
929+
930+ Example::
931+
932+ with LoggerStream.replace_print_ctx(skip_same_head=False):
933+ print("This is a log message.")
934+ print("This is another log message.")
935+ # 24-08-10 19:30:07 This is a log message.
936+ # 24-08-10 19:30:07 This is another log message.
937+ # Back to original print function
938+ print("This is a normal print message.")
939+ # This is a normal print message.
940+ """
941+ try :
942+ cls .install_print (
943+ skip_same_head = skip_same_head ,
944+ writer = writer ,
945+ flush = flush ,
946+ get_prefix = get_prefix ,
947+ )
948+ yield
949+ finally :
950+ cls .restore_print ()
951+
952+ @classmethod
953+ def restore_print (cls ):
954+ import builtins
955+
956+ builtins .print = cls .BUILTINS_PRINT
957+
958+ @classmethod
959+ def install_print (
960+ cls ,
961+ skip_same_head = True ,
962+ writer = BUILTINS_STDERR .write ,
963+ flush = BUILTINS_STDERR .flush ,
964+ get_prefix = None ,
965+ ):
966+ """Install a custom print function that writes to LoggerStream.
967+
968+ Args:
969+ skip_same_head (bool, optional): Whether to skip logging the same message head. Defaults to True.
970+ writer (_type_, optional): Writer function. Defaults to BUILTINS_STDERR.write.
971+ flush (_type_, optional): Flush function. Defaults to BUILTINS_STDERR.flush.
972+
973+ Example::
974+
975+ # 1. Basic usage
976+ LoggerStream.install_print()
977+ print("This is a log message.")
978+ print("This is another log message.")
979+ # 24-08-10 19:30:07 This is a log message.
980+ # This is another log message.
981+ LoggerStream.restore_print()
982+
983+ # 2. Log to a file
984+ log_file = open("log.txt", "a", encoding="utf-8")
985+ LoggerStream.install_print(writer=log_file.write, flush=log_file.flush)
986+ print("This is a log message to file.")
987+ LoggerStream.restore_print()
988+
989+ # 3. Log to a custom logger
990+ logger = logging.getLogger("my_logger")
991+ LoggerStream.install_print(writer=logger.info)
992+ print("This is a log message to logger.")
993+ LoggerStream.restore_print()
994+
995+ # 4. Using context manager
996+ with LoggerStream.replace_print_ctx(skip_same_head=False):
997+ print("This is a log message.")
998+ print("This is another log message.")
999+ # 24-08-10 19:30:07 This is a log message.
1000+ # 24-08-10 19:30:07 This is another log message
1001+ # Back to original print function
1002+ print("This is a normal print message.")
1003+ # This is a normal print message.
1004+ """
1005+ import builtins
1006+
1007+ logger_stream = cls (
1008+ skip_same_head = skip_same_head ,
1009+ writer = writer ,
1010+ flush = flush ,
1011+ get_prefix = get_prefix ,
1012+ )
1013+
1014+ def custom_print (* values , sep = " " , end = "\n " , file = None , flush = False ):
1015+ if file is None :
1016+ # redirect to our logger_stream only when file is None
1017+ message = f"{ sep .join (str (arg ) for arg in values )} { end } "
1018+ logger_stream .write (message )
1019+ if flush :
1020+ logger_stream .flush ()
1021+ else :
1022+ cls .BUILTINS_PRINT (* values , sep = sep , end = end , file = file , flush = flush )
1023+
1024+ builtins .print = custom_print
1025+
1026+
8221027def test_LogHelper ():
8231028 logger = LogHelper .bind_handler (
8241029 "app_test" , filename = "app_test.log" , maxBytes = 1 , backupCount = 2
@@ -863,6 +1068,32 @@ async def _test():
8631068 assert asyncio .run (_test ()) is True
8641069
8651070
1071+ def test_LoggerStream ():
1072+ cache = []
1073+
1074+ def get_prefix ():
1075+ return "PREFIX "
1076+
1077+ def writer (msg : str ):
1078+ cache .append (msg )
1079+
1080+ with LoggerStream .replace_print_ctx (
1081+ skip_same_head = True , writer = writer , get_prefix = get_prefix
1082+ ):
1083+ print ("123" )
1084+ print ("456" )
1085+ assert cache == ["PREFIX 123\n " , "456\n " ], cache
1086+ cache .clear ()
1087+
1088+ with LoggerStream .replace_print_ctx (
1089+ skip_same_head = False , writer = writer , get_prefix = get_prefix
1090+ ):
1091+ print ("123" )
1092+ print ("456" )
1093+ assert cache == ["PREFIX 123\n " , "PREFIX 456\n " ], cache
1094+ cache .clear ()
1095+
1096+
8661097def test_utils ():
8671098 test_LogHelper ()
8681099 print (time .strftime ("%Y-%m-%d %H:%M:%S" , time .localtime ()), "test_LogHelper passed" )
@@ -871,6 +1102,10 @@ def test_utils():
8711102 time .strftime ("%Y-%m-%d %H:%M:%S" , time .localtime ()),
8721103 "test_AsyncQueueListener passed" ,
8731104 )
1105+ test_LoggerStream ()
1106+ print (
1107+ time .strftime ("%Y-%m-%d %H:%M:%S" , time .localtime ()), "test_LoggerStream passed"
1108+ )
8741109
8751110
8761111def test ():
0 commit comments