docs for muutils v0.9.1
View Source on GitHub

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: str name of the stream
  • aliases: 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 to name + ".log"
    • if False will "write" to NullIO (throw it away)
    • if a string, will write to that file
    • if a fileIO type object, will write to that object
  • default_level: int|None default level for this stream
  • default_contents: dict[str, Callable[[], Any]] default contents for this stream
  • last_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)
name: str | None
aliases: set[str | None]
file: Union[str, bool, TextIO, muutils.logger.simplelogger.NullIO, NoneType] = None
default_level: int | None = None
default_contents: dict[str, typing.Callable[[], typing.Any]]
handler: Union[TextIO, muutils.logger.simplelogger.NullIO, NoneType] = None
def make_handler(self) -> Union[TextIO, muutils.logger.simplelogger.NullIO, NoneType]:
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