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