muutils.logger.loggingstream
1from __future__ import annotations 2 3import sys 4import time 5from dataclasses import dataclass, field 6from typing import Any, Callable 7 8if sys.version_info >= (3, 12): 9 from typing import override 10else: 11 from typing_extensions import override 12 13from muutils.logger.simplelogger import AnyIO, NullIO 14from muutils.misc import sanitize_fname 15 16 17@dataclass 18class LoggingStream: 19 """properties of a logging stream 20 21 - `name: str` name of the stream 22 - `aliases: set[str]` aliases for the stream 23 (calls to these names will be redirected to this stream. duplicate alises will result in errors) 24 TODO: perhaps duplicate alises should result in duplicate writes? 25 - `file: str|bool|AnyIO|None` file to write to 26 - if `None`, will write to standard log 27 - if `True`, will write to `name + ".log"` 28 - if `False` will "write" to `NullIO` (throw it away) 29 - if a string, will write to that file 30 - if a fileIO type object, will write to that object 31 - `default_level: int|None` default level for this stream 32 - `default_contents: dict[str, Callable[[], Any]]` default contents for this stream 33 - `last_msg: tuple[float, Any]|None` last message written to this stream (timestamp, message) 34 """ 35 36 name: str | None 37 aliases: set[str | None] = field(default_factory=set) 38 file: str | bool | AnyIO | None = None 39 default_level: int | None = None 40 default_contents: dict[str, Callable[[], Any]] = field(default_factory=dict) 41 handler: AnyIO | None = None 42 43 # TODO: implement last-message caching 44 # last_msg: tuple[float, Any]|None = None 45 46 def make_handler(self) -> AnyIO | None: 47 if self.file is None: 48 return None 49 elif isinstance(self.file, str): 50 # if its a string, open a file 51 return open( 52 self.file, 53 "w", 54 encoding="utf-8", 55 ) 56 elif isinstance(self.file, bool): 57 # if its a bool and true, open a file with the same name as the stream (in the current dir) 58 # TODO: make this happen in the same dir as the main logfile? 59 if self.file: 60 return open( # type: ignore[return-value] 61 f"{sanitize_fname(self.name)}.log.jsonl", 62 "w", 63 encoding="utf-8", 64 ) 65 else: 66 return NullIO() 67 else: 68 # if its neither, check it has `.write()` and `.flush()` methods 69 if ( 70 ( 71 not hasattr(self.file, "write") 72 or (not callable(self.file.write)) 73 or (not hasattr(self.file, "flush")) 74 or (not callable(self.file.flush)) 75 ) 76 or (not hasattr(self.file, "close")) 77 or (not callable(self.file.close)) 78 ): 79 raise ValueError(f"stream {self.name} has invalid handler {self.file}") 80 # ignore type check because we know it has a .write() method, 81 # assume the user knows what they're doing 82 return self.file # type: ignore 83 84 def __post_init__(self): 85 self.aliases = set(self.aliases) 86 if any(x.startswith("_") for x in self.aliases if x is not None): 87 raise ValueError( 88 "stream names or aliases cannot start with an underscore, sorry" 89 ) 90 self.aliases.add(self.name) 91 self.default_contents["_timestamp"] = time.time 92 self.default_contents["_stream"] = lambda: self.name 93 self.handler = self.make_handler() 94 95 def __del__(self): 96 if self.handler is not None: 97 self.handler.flush() 98 self.handler.close() 99 100 @override 101 def __str__(self): 102 return f"LoggingStream(name={self.name}, aliases={self.aliases}, file={self.file}, default_level={self.default_level}, default_contents={self.default_contents})"
@dataclass
class
LoggingStream:
18@dataclass 19class LoggingStream: 20 """properties of a logging stream 21 22 - `name: str` name of the stream 23 - `aliases: set[str]` aliases for the stream 24 (calls to these names will be redirected to this stream. duplicate alises will result in errors) 25 TODO: perhaps duplicate alises should result in duplicate writes? 26 - `file: str|bool|AnyIO|None` file to write to 27 - if `None`, will write to standard log 28 - if `True`, will write to `name + ".log"` 29 - if `False` will "write" to `NullIO` (throw it away) 30 - if a string, will write to that file 31 - if a fileIO type object, will write to that object 32 - `default_level: int|None` default level for this stream 33 - `default_contents: dict[str, Callable[[], Any]]` default contents for this stream 34 - `last_msg: tuple[float, Any]|None` last message written to this stream (timestamp, message) 35 """ 36 37 name: str | None 38 aliases: set[str | None] = field(default_factory=set) 39 file: str | bool | AnyIO | None = None 40 default_level: int | None = None 41 default_contents: dict[str, Callable[[], Any]] = field(default_factory=dict) 42 handler: AnyIO | None = None 43 44 # TODO: implement last-message caching 45 # last_msg: tuple[float, Any]|None = None 46 47 def make_handler(self) -> AnyIO | None: 48 if self.file is None: 49 return None 50 elif isinstance(self.file, str): 51 # if its a string, open a file 52 return open( 53 self.file, 54 "w", 55 encoding="utf-8", 56 ) 57 elif isinstance(self.file, bool): 58 # if its a bool and true, open a file with the same name as the stream (in the current dir) 59 # TODO: make this happen in the same dir as the main logfile? 60 if self.file: 61 return open( # type: ignore[return-value] 62 f"{sanitize_fname(self.name)}.log.jsonl", 63 "w", 64 encoding="utf-8", 65 ) 66 else: 67 return NullIO() 68 else: 69 # if its neither, check it has `.write()` and `.flush()` methods 70 if ( 71 ( 72 not hasattr(self.file, "write") 73 or (not callable(self.file.write)) 74 or (not hasattr(self.file, "flush")) 75 or (not callable(self.file.flush)) 76 ) 77 or (not hasattr(self.file, "close")) 78 or (not callable(self.file.close)) 79 ): 80 raise ValueError(f"stream {self.name} has invalid handler {self.file}") 81 # ignore type check because we know it has a .write() method, 82 # assume the user knows what they're doing 83 return self.file # type: ignore 84 85 def __post_init__(self): 86 self.aliases = set(self.aliases) 87 if any(x.startswith("_") for x in self.aliases if x is not None): 88 raise ValueError( 89 "stream names or aliases cannot start with an underscore, sorry" 90 ) 91 self.aliases.add(self.name) 92 self.default_contents["_timestamp"] = time.time 93 self.default_contents["_stream"] = lambda: self.name 94 self.handler = self.make_handler() 95 96 def __del__(self): 97 if self.handler is not None: 98 self.handler.flush() 99 self.handler.close() 100 101 @override 102 def __str__(self): 103 return f"LoggingStream(name={self.name}, aliases={self.aliases}, file={self.file}, default_level={self.default_level}, default_contents={self.default_contents})"
properties of a logging stream
name: strname of the streamaliases: set[str]aliases for the stream (calls to these names will be redirected to this stream. duplicate alises will result in errors) TODO: perhaps duplicate alises should result in duplicate writes?file: str|bool|AnyIO|Nonefile to write to- if
None, will write to standard log - if
True, will write toname + ".log" - if
Falsewill "write" toNullIO(throw it away) - if a string, will write to that file
- if a fileIO type object, will write to that object
- if
default_level: int|Nonedefault level for this streamdefault_contents: dict[str, Callable[[], Any]]default contents for this streamlast_msg: tuple[float, Any]|Nonelast message written to this stream (timestamp, message)
LoggingStream( name: str | None, aliases: set[str | None] = <factory>, file: Union[str, bool, TextIO, muutils.logger.simplelogger.NullIO, NoneType] = None, default_level: int | None = None, default_contents: dict[str, typing.Callable[[], typing.Any]] = <factory>, handler: Union[TextIO, muutils.logger.simplelogger.NullIO, NoneType] = None)
47 def make_handler(self) -> AnyIO | None: 48 if self.file is None: 49 return None 50 elif isinstance(self.file, str): 51 # if its a string, open a file 52 return open( 53 self.file, 54 "w", 55 encoding="utf-8", 56 ) 57 elif isinstance(self.file, bool): 58 # if its a bool and true, open a file with the same name as the stream (in the current dir) 59 # TODO: make this happen in the same dir as the main logfile? 60 if self.file: 61 return open( # type: ignore[return-value] 62 f"{sanitize_fname(self.name)}.log.jsonl", 63 "w", 64 encoding="utf-8", 65 ) 66 else: 67 return NullIO() 68 else: 69 # if its neither, check it has `.write()` and `.flush()` methods 70 if ( 71 ( 72 not hasattr(self.file, "write") 73 or (not callable(self.file.write)) 74 or (not hasattr(self.file, "flush")) 75 or (not callable(self.file.flush)) 76 ) 77 or (not hasattr(self.file, "close")) 78 or (not callable(self.file.close)) 79 ): 80 raise ValueError(f"stream {self.name} has invalid handler {self.file}") 81 # ignore type check because we know it has a .write() method, 82 # assume the user knows what they're doing 83 return self.file # type: ignore